WebAgent/src/pages/llm-chat/index.vue

229 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { ChatMessage } from '@/types/chat'
import { useSessionManager } from '@/composables/useSessionManager'
import SessionSelector from '@/components/SessionSelector.vue'
import { useChatSessionStore } from '@/stores/modules/chatSession'
import { useScroll } from '@vueuse/core'
import { streamApi } from '@/api/agent'
const sessionManager = useSessionManager()
sessionManager.initialize()
const sessionStore = useChatSessionStore()
// Messages from current session - get directly from store
const messages = computed(() => sessionStore.currentSession?.messages || [])
const inputText = ref('')
const isLoading = ref(false)
const messagesContainer = ref<HTMLElement>()
// 流式响应相关状态
const currentAiMessage = ref<ChatMessage | null>(null)
const isStreaming = ref(false)
const streamController = ref<AbortController | null>(null)
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) {
if (!text.trim())
return
// 添加用户消息到当前会话
const userMessage: ChatMessage = {
id: Date.now(),
content: text,
sender: 'user',
timestamp: new Date(),
status: 'sent',
}
const memory = sessionManager.handleNewMessage(userMessage)
// 创建 AI 消息占位符
const aiMessage: ChatMessage = {
id: Date.now() + 1,
content: '',
sender: 'ai',
timestamp: new Date(),
status: 'sending',
}
sessionManager.handleNewMessage(aiMessage)
// 设置当前 AI 消息引用(获取最后一条消息)
const currentSession = sessionStore.currentSession
const sessionId = sessionStore.currentSessionId
if (currentSession && currentSession.messages.length > 0) {
currentAiMessage.value = currentSession.messages[currentSession.messages.length - 1]
}
// 清空输入
inputText.value = ''
// 设置加载状态
isLoading.value = true
isStreaming.value = true
// 创建 AbortController
const controller = new AbortController()
streamController.value = controller
// 调用流式 API传递 memory 参数
streamApi(
{
message: text,
options: memory ? { memory } : undefined
},
{
onopen(response) {
console.log('SSE 连接已建立:', response)
},
onmessage(data) {
try {
const parsed = JSON.parse(data) as { text: string }
if (parsed.text && currentAiMessage.value && sessionId) {
// 使用 store 更新消息内容
sessionStore.updateMessageContent(sessionId, currentAiMessage.value.id, parsed.text)
// 更新本地引用
currentAiMessage.value.content += 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 completeMessage() {
if (currentAiMessage.value && sessionStore.currentSession) {
const sessionId = sessionStore.currentSessionId
const messageId = currentAiMessage.value.id
// 更新消息状态为 'sent'
const session = sessionStore.currentSession
if (session && sessionId) {
const updatedMessages = session.messages.map(msg =>
msg.id === messageId ? { ...msg, status: 'sent' as const } : msg
)
sessionStore.updateSession(sessionId, { messages: updatedMessages })
}
}
cleanupStream()
}
// 处理流式错误
function handleStreamError(error: Error) {
if (currentAiMessage.value && sessionStore.currentSession) {
const sessionId = sessionStore.currentSessionId
const messageId = currentAiMessage.value.id
// 更新消息状态为 'error' 并添加错误标记
const session = sessionStore.currentSession
if (session && sessionId) {
const updatedMessages = session.messages.map(msg => {
if (msg.id === messageId) {
return {
...msg,
status: 'error' as const,
content: msg.content + ' (连接错误)'
}
}
return msg
})
sessionStore.updateSession(sessionId, { messages: updatedMessages })
}
}
// 显示错误提示(可集成 vant 的 Toast 组件)
console.error('AI 响应错误:', error)
cleanupStream()
}
// 清理流式连接
function cleanupStream() {
isLoading.value = false
isStreaming.value = false
streamController.value = null
currentAiMessage.value = null
}
function handleKeydown(e: KeyboardEvent) {
// You can add additional keyboard shortcuts here
console.log('Keydown event:', e.key)
}
</script>
<template>
<div class="bg-gray-50 flex flex-col h-screen dark:bg-gray-950">
<!-- Session selector -->
<SessionSelector />
<!-- Messages container -->
<div
ref="messagesContainer"
class="p-4 pb-24 flex-1 overflow-auto"
>
<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>
</div>
<!-- Input area -->
<div class="bottom-0 left-0 right-0 fixed z-10">
<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
}
}
</route>