Vrda's picture
Feature: EN/HR language toggle on login + full app i18n
81046e2
<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>