<template>
<div
class="input-bar input-dropzone"
:class="{ 'is-dragging': isDragging }"
@dragenter="handleDragEnter"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<!-- File previews -->
<FilePreviewStrip
v-if="chat.pendingImages.length || chat.pendingFiles.length"
/>
<!-- Upload progress -->
<div v-if="uploading" class="upload-progress">
<div class="upload-progress-fill" style="width: 60%" />
</div>
<!-- Reconnect warning -->
<div v-if="props.ws.reconnectFailed.value" class="reconnect-notice">
<i class="ph ph-warning"></i>
Connection lost — retrying in background
</div>
<div class="input-row">
<input
ref="fileInputEl"
type="file"
multiple
style="display:none"
@change="handleFileInput"
/>
<button
class="btn-icon"
title="Attach files"
:disabled="!chat.currentId"
@click="fileInputEl.click()"
>
<i class="ph ph-paperclip"></i>
</button>
<textarea
ref="textareaEl"
v-model="draft"
class="input-textarea"
placeholder="Message Navi…"
rows="1"
:disabled="!chat.currentId"
@keydown.enter.exact.prevent="handleSend"
@keydown.ctrl.enter.prevent="insertNewline"
@input="autoResize"
@paste="handlePaste"
/>
<button
v-if="chat.streaming"
class="btn-icon"
title="Stop generation"
@click="handleStop"
>
<i class="ph ph-stop-circle"></i>
</button>
<button
v-else
class="btn-icon"
title="Send (Enter)"
:disabled="!canSend"
@click="handleSend"
>
<i class="ph ph-paper-plane-tilt"></i>
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { useChatStore } from '@/stores/chat.js'
import { useFileUpload } from '@/composables/useFileUpload.js'
import { stopSession } from '@/api/index.js'
import FilePreviewStrip from './FilePreviewStrip.vue'
const props = defineProps({
ws: { type: Object, required: true }
})
const chat = useChatStore()
// Destructure so refs are top-level → auto-unwrapped in template
const {
isDragging, uploading,
handleFileInput, handlePaste,
handleDragEnter, handleDragOver, handleDragLeave, handleDrop,
clearPending
} = useFileUpload()
const draft = ref('')
const textareaEl = ref(null)
const fileInputEl = ref(null)
const canSend = computed(() =>
!!chat.currentId && !chat.streaming &&
(draft.value.trim() || chat.pendingImages.length || chat.pendingFiles.length)
)
chat.onDraftRestore((saved) => {
draft.value = saved
nextTick(autoResize)
})
watch(draft, (val) => chat.saveDraft(val))
function autoResize() {
const el = textareaEl.value
if (!el) return
el.style.height = 'auto'
el.style.height = Math.min(el.scrollHeight, 144) + 'px'
}
function handleSend() {
if (!canSend.value) return
const text = draft.value.trim()
const images = [...chat.pendingImages]
const files = [...chat.pendingFiles]
chat.appendUserMessage(text, images, files)
clearPending()
draft.value = ''
nextTick(autoResize)
// Strip data URL prefix — server expects raw base64
const b64Images = images.map(d => d.split(',', 2)[1]).filter(Boolean)
const payload = { type: 'message', content: text || ' ' }
if (b64Images.length) payload.images = b64Images
if (files.length) payload.files = files
props.ws.send(payload)
}
function insertNewline() {
const el = textareaEl.value
if (!el) return
const start = el.selectionStart
const end = el.selectionEnd
draft.value = draft.value.slice(0, start) + '\n' + draft.value.slice(end)
nextTick(() => {
el.selectionStart = el.selectionEnd = start + 1
autoResize()
})
}
async function handleStop() {
if (!chat.currentId) return
await stopSession(chat.currentId)
}
</script>
<style scoped>
.reconnect-notice {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
font-size: 12px;
color: var(--color-warning, #e0af68);
margin-bottom: 4px;
}
</style>