Spaces:
Running
Running
| <script setup lang="ts"> | |
| import { ref, nextTick, watch } from 'vue' | |
| import { marked } from 'marked' | |
| import type { ChatMessage } from '../types' | |
| import type { Lang } from '../i18n' | |
| import { t } from '../i18n' | |
| import { sendChat } from '../api' | |
| import Wc3Button from '../wc3/base/Button.vue' | |
| import CitationPanel from './CitationPanel.vue' | |
| const props = defineProps<{ | |
| model: string | |
| showCitations: boolean | |
| lang: Lang | |
| }>() | |
| const history = defineModel<ChatMessage[]>('history', { required: true }) | |
| const inputText = ref('') | |
| const isLoading = ref(false) | |
| const error = ref('') | |
| const messagesEl = ref<HTMLElement | null>(null) | |
| marked.setOptions({ breaks: true, gfm: true }) | |
| function renderMd(text: string): string { | |
| return marked.parse(text) as string | |
| } | |
| async function scrollToBottom() { | |
| await nextTick() | |
| if (messagesEl.value) { | |
| messagesEl.value.scrollTop = messagesEl.value.scrollHeight | |
| } | |
| } | |
| watch(() => history.value.length, scrollToBottom) | |
| async function handleSend() { | |
| const text = inputText.value.trim() | |
| if (!text || isLoading.value) return | |
| inputText.value = '' | |
| error.value = '' | |
| history.value.push({ role: 'user', content: text }) | |
| await scrollToBottom() | |
| isLoading.value = true | |
| try { | |
| const res = await sendChat(text, props.model, history.value, props.lang) | |
| history.value.push({ | |
| role: 'assistant', | |
| content: res.reply, | |
| citations: res.citations, | |
| model_used: res.model_used, | |
| }) | |
| } catch (e: any) { | |
| error.value = e.message || t('chat.error', props.lang) | |
| } finally { | |
| isLoading.value = false | |
| await scrollToBottom() | |
| } | |
| } | |
| function handleKeydown(e: KeyboardEvent) { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault() | |
| handleSend() | |
| } | |
| } | |
| </script> | |
| <template> | |
| <div class="chat-area"> | |
| <!-- Messages --> | |
| <div class="messages-container" ref="messagesEl"> | |
| <!-- Empty --> | |
| <div v-if="history.length === 0" style="text-align: center; margin-top: 3rem;"> | |
| <div style="font-size: 3rem; margin-bottom: 0.75rem;">📜</div> | |
| <p style="font-family: var(--font-heading); color: var(--wc3-gold); font-size: 1rem;"> | |
| {{ t('chat.welcome', lang) }} | |
| </p> | |
| <p style="color: var(--wc3-text-dim); font-size: 0.9rem; max-width: 400px; margin: 0.5rem auto;"> | |
| {{ t('chat.welcomeHint', lang) }} | |
| </p> | |
| </div> | |
| <template v-for="(msg, idx) in history" :key="idx"> | |
| <div class="msg" :class="msg.role"> | |
| <div v-html="renderMd(msg.content)"></div> | |
| <div class="msg-meta" v-if="msg.model_used"> | |
| {{ msg.model_used }} | |
| </div> | |
| <CitationPanel | |
| v-if="msg.role === 'assistant' && showCitations && msg.citations?.length" | |
| :citations="msg.citations" | |
| :lang="lang" | |
| /> | |
| </div> | |
| </template> | |
| <div v-if="isLoading" class="loading"> | |
| <span>{{ t('chat.generating', lang) }}</span> | |
| <span class="dot-pulse"><span></span><span></span><span></span></span> | |
| </div> | |
| <div v-if="error" class="error-msg">⚠️ {{ error }}</div> | |
| </div> | |
| <!-- Input --> | |
| <div class="chat-input-bar"> | |
| <input | |
| class="wc3-input" | |
| v-model="inputText" | |
| @keydown="handleKeydown" | |
| :placeholder="t('chat.placeholder', lang)" | |
| :disabled="isLoading" | |
| /> | |
| <div class="chat-send-btn"> | |
| <Wc3Button size="s" @click="handleSend" :disabled="isLoading || !inputText.trim()"> | |
| {{ t('chat.send', lang) }} | |
| </Wc3Button> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <style scoped> | |
| .chat-area { | |
| flex: 1; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .messages-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 1rem 1.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| </style> | |