2025-12-18 01:00:25 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import type { ChatMessage } from '@/types/chat'
|
|
|
|
|
|
import { useScroll } from '@vueuse/core'
|
2025-12-19 18:02:31 +08:00
|
|
|
|
import { streamApi } from '@/api/agent'
|
2025-12-18 01:00:25 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Sample chat data for demonstration
|
|
|
|
|
|
const messages = ref<ChatMessage[]>([
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const inputText = ref('')
|
|
|
|
|
|
const isLoading = ref(false)
|
|
|
|
|
|
const messagesContainer = ref<HTMLElement>()
|
|
|
|
|
|
|
2025-12-19 18:02:31 +08:00
|
|
|
|
// 流式响应相关状态
|
|
|
|
|
|
const currentAiMessage = ref<ChatMessage | null>(null)
|
|
|
|
|
|
const isStreaming = ref(false)
|
|
|
|
|
|
const streamController = ref<AbortController | null>(null)
|
|
|
|
|
|
|
2025-12-18 01:00:25 +08:00
|
|
|
|
const { arrivedState } = useScroll(messagesContainer, {
|
|
|
|
|
|
offset: { bottom: 100 },
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-scroll to bottom when new messages arrive
|
|
|
|
|
|
watch(messages, () => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (messagesContainer.value && arrivedState.bottom) {
|
|
|
|
|
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}, { deep: true })
|
|
|
|
|
|
|
|
|
|
|
|
function handleSend(text: string) {
|
refactor: remove i18n dependency and hardcode strings in components
- Updated ChatInput.vue to remove i18n and handle model value updates directly.
- Refactored NavBar.vue to use a static title map instead of i18n.
- Simplified TabBar.vue by replacing i18n calls with hardcoded titles.
- Removed i18n usage in various pages (charts, counter, forgot-password, index, keepalive, llm-chat, login, mock, profile, register, scroll-cache, settings, unocss).
- Deleted localization files (en-US.json, zh-CN.json) and i18n utility functions.
- Updated constants to provide static app name and description.
- Adjusted page titles in set-page-title.ts to use static names.
- Cleaned up TypeScript types by removing unused i18n types.
2025-12-19 10:09:47 +08:00
|
|
|
|
if (!text.trim())
|
|
|
|
|
|
return
|
2025-12-18 01:00:25 +08:00
|
|
|
|
|
2025-12-19 18:02:31 +08:00
|
|
|
|
// 添加用户消息
|
2025-12-18 01:00:25 +08:00
|
|
|
|
const userMessage: ChatMessage = {
|
|
|
|
|
|
id: Date.now(),
|
|
|
|
|
|
content: text,
|
|
|
|
|
|
sender: 'user',
|
|
|
|
|
|
timestamp: new Date(),
|
refactor: remove i18n dependency and hardcode strings in components
- Updated ChatInput.vue to remove i18n and handle model value updates directly.
- Refactored NavBar.vue to use a static title map instead of i18n.
- Simplified TabBar.vue by replacing i18n calls with hardcoded titles.
- Removed i18n usage in various pages (charts, counter, forgot-password, index, keepalive, llm-chat, login, mock, profile, register, scroll-cache, settings, unocss).
- Deleted localization files (en-US.json, zh-CN.json) and i18n utility functions.
- Updated constants to provide static app name and description.
- Adjusted page titles in set-page-title.ts to use static names.
- Cleaned up TypeScript types by removing unused i18n types.
2025-12-19 10:09:47 +08:00
|
|
|
|
status: 'sent',
|
2025-12-18 01:00:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(userMessage)
|
|
|
|
|
|
|
2025-12-19 18:02:31 +08:00
|
|
|
|
// 创建 AI 消息占位符
|
|
|
|
|
|
const aiMessage: ChatMessage = {
|
|
|
|
|
|
id: Date.now() + 1,
|
|
|
|
|
|
content: '',
|
|
|
|
|
|
sender: 'ai',
|
|
|
|
|
|
timestamp: new Date(),
|
|
|
|
|
|
status: 'sending',
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(aiMessage)
|
|
|
|
|
|
currentAiMessage.value = aiMessage
|
|
|
|
|
|
|
|
|
|
|
|
// 清空输入
|
2025-12-18 01:00:25 +08:00
|
|
|
|
inputText.value = ''
|
|
|
|
|
|
|
2025-12-19 18:02:31 +08:00
|
|
|
|
// 设置加载状态
|
2025-12-18 01:00:25 +08:00
|
|
|
|
isLoading.value = true
|
2025-12-19 18:02:31 +08:00
|
|
|
|
isStreaming.value = true
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 AbortController(虽然用户不需要中止功能,但保留以备后用)
|
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
streamController.value = controller
|
|
|
|
|
|
|
|
|
|
|
|
// 调用流式 API
|
|
|
|
|
|
streamApi(
|
|
|
|
|
|
text,
|
|
|
|
|
|
{
|
|
|
|
|
|
onopen(response) {
|
|
|
|
|
|
console.log('SSE 连接已建立:', response)
|
|
|
|
|
|
},
|
|
|
|
|
|
onmessage(data) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(data) as { text: string }
|
|
|
|
|
|
if (parsed.text) {
|
|
|
|
|
|
// 打字机效果:逐字符追加
|
|
|
|
|
|
typewriterEffect(parsed.text)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('解析 SSE 数据失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onclose() {
|
|
|
|
|
|
console.log('SSE 连接已关闭')
|
|
|
|
|
|
completeMessage()
|
|
|
|
|
|
},
|
|
|
|
|
|
onerror(error) {
|
|
|
|
|
|
console.error('SSE 连接错误:', error)
|
|
|
|
|
|
handleStreamError(error)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ signal: controller.signal }
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打字机效果:逐字符显示
|
|
|
|
|
|
function typewriterEffect(text: string) {
|
|
|
|
|
|
if (!currentAiMessage.value)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
const duration = 100 // 每个字符间隔(毫秒)
|
|
|
|
|
|
const fullText = currentAiMessage.value.content + text
|
|
|
|
|
|
|
|
|
|
|
|
function updateChar(index: number) {
|
|
|
|
|
|
if (!currentAiMessage.value)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if (index < fullText.length) {
|
|
|
|
|
|
currentAiMessage.value.content = fullText.slice(0, index + 1)
|
|
|
|
|
|
// 触发 Vue 响应式更新
|
|
|
|
|
|
const messageIndex = messages.value.findIndex(m => m.id === currentAiMessage.value?.id)
|
|
|
|
|
|
if (messageIndex !== -1) {
|
|
|
|
|
|
messages.value[messageIndex] = { ...currentAiMessage.value }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => updateChar(index + 1), duration)
|
2025-12-18 01:00:25 +08:00
|
|
|
|
}
|
2025-12-19 18:02:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateChar(currentAiMessage.value.content.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 完成消息处理
|
|
|
|
|
|
function completeMessage() {
|
|
|
|
|
|
if (currentAiMessage.value) {
|
|
|
|
|
|
currentAiMessage.value.status = 'sent'
|
|
|
|
|
|
const messageIndex = messages.value.findIndex(m => m.id === currentAiMessage.value?.id)
|
|
|
|
|
|
if (messageIndex !== -1) {
|
|
|
|
|
|
messages.value[messageIndex] = { ...currentAiMessage.value }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cleanupStream()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理流式错误
|
|
|
|
|
|
function handleStreamError(error: Error) {
|
|
|
|
|
|
if (currentAiMessage.value) {
|
|
|
|
|
|
currentAiMessage.value.status = 'error'
|
|
|
|
|
|
currentAiMessage.value.content += ' (连接错误)'
|
|
|
|
|
|
const messageIndex = messages.value.findIndex(m => m.id === currentAiMessage.value?.id)
|
|
|
|
|
|
if (messageIndex !== -1) {
|
|
|
|
|
|
messages.value[messageIndex] = { ...currentAiMessage.value }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示错误提示(可集成 vant 的 Toast 组件)
|
|
|
|
|
|
console.error('AI 响应错误:', error)
|
|
|
|
|
|
cleanupStream()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理流式连接
|
|
|
|
|
|
function cleanupStream() {
|
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
|
streamController.value = null
|
|
|
|
|
|
currentAiMessage.value = null
|
2025-12-18 01:00:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
|
|
|
|
// You can add additional keyboard shortcuts here
|
|
|
|
|
|
console.log('Keydown event:', e.key)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
refactor: remove i18n dependency and hardcode strings in components
- Updated ChatInput.vue to remove i18n and handle model value updates directly.
- Refactored NavBar.vue to use a static title map instead of i18n.
- Simplified TabBar.vue by replacing i18n calls with hardcoded titles.
- Removed i18n usage in various pages (charts, counter, forgot-password, index, keepalive, llm-chat, login, mock, profile, register, scroll-cache, settings, unocss).
- Deleted localization files (en-US.json, zh-CN.json) and i18n utility functions.
- Updated constants to provide static app name and description.
- Adjusted page titles in set-page-title.ts to use static names.
- Cleaned up TypeScript types by removing unused i18n types.
2025-12-19 10:09:47 +08:00
|
|
|
|
<div class="bg-gray-50 flex flex-col h-screen dark:bg-gray-950">
|
2025-12-18 01:00:25 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- Messages container -->
|
|
|
|
|
|
<van-list
|
|
|
|
|
|
ref="messagesContainer"
|
2025-12-19 17:03:54 +08:00
|
|
|
|
class="p-4 pb-24 flex-1"
|
2025-12-18 01:00:25 +08:00
|
|
|
|
:finished="true"
|
|
|
|
|
|
:loading="false"
|
|
|
|
|
|
finished-text=""
|
|
|
|
|
|
loading-text=""
|
|
|
|
|
|
>
|
|
|
|
|
|
<van-empty
|
|
|
|
|
|
v-if="messages.length === 0"
|
|
|
|
|
|
description="No messages yet. Start a conversation!"
|
|
|
|
|
|
class="mt-20"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else>
|
|
|
|
|
|
<ChatBubble
|
|
|
|
|
|
v-for="message in messages"
|
|
|
|
|
|
:key="message.id"
|
|
|
|
|
|
:message="message"
|
|
|
|
|
|
:show-timestamp="true"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</van-list>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Input area -->
|
refactor: remove i18n dependency and hardcode strings in components
- Updated ChatInput.vue to remove i18n and handle model value updates directly.
- Refactored NavBar.vue to use a static title map instead of i18n.
- Simplified TabBar.vue by replacing i18n calls with hardcoded titles.
- Removed i18n usage in various pages (charts, counter, forgot-password, index, keepalive, llm-chat, login, mock, profile, register, scroll-cache, settings, unocss).
- Deleted localization files (en-US.json, zh-CN.json) and i18n utility functions.
- Updated constants to provide static app name and description.
- Adjusted page titles in set-page-title.ts to use static names.
- Cleaned up TypeScript types by removing unused i18n types.
2025-12-19 10:09:47 +08:00
|
|
|
|
<div class="bottom-0 left-0 right-0 fixed z-10">
|
2025-12-18 01:00:25 +08:00
|
|
|
|
<ChatInput
|
|
|
|
|
|
v-model="inputText"
|
|
|
|
|
|
:disabled="isLoading"
|
|
|
|
|
|
:loading="isLoading"
|
|
|
|
|
|
placeholder="Type your message..."
|
|
|
|
|
|
@send="handleSend"
|
|
|
|
|
|
@keydown="handleKeydown"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<route lang="json5">
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'LLMChat',
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: 'AI Chat',
|
|
|
|
|
|
requiresAuth: false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: remove i18n dependency and hardcode strings in components
- Updated ChatInput.vue to remove i18n and handle model value updates directly.
- Refactored NavBar.vue to use a static title map instead of i18n.
- Simplified TabBar.vue by replacing i18n calls with hardcoded titles.
- Removed i18n usage in various pages (charts, counter, forgot-password, index, keepalive, llm-chat, login, mock, profile, register, scroll-cache, settings, unocss).
- Deleted localization files (en-US.json, zh-CN.json) and i18n utility functions.
- Updated constants to provide static app name and description.
- Adjusted page titles in set-page-title.ts to use static names.
- Cleaned up TypeScript types by removing unused i18n types.
2025-12-19 10:09:47 +08:00
|
|
|
|
</route>
|