feat(llm-chat): 实现流式API响应和打字机效果

添加流式API调用功能,替换原有的模拟响应。实现以下功能:
1. 使用SSE连接处理AI响应
2. 添加打字机效果逐字显示AI回复
3. 增加错误处理和连接管理
4. 移除未使用的Vant组件声明
This commit is contained in:
dzq 2025-12-19 18:02:31 +08:00
parent 935c662203
commit b9d7f495ad
2 changed files with 117 additions and 17 deletions

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ChatMessage } from '@/types/chat'
import { useScroll } from '@vueuse/core'
import { isDark, toggleDark } from '@/composables/dark'
import { streamApi } from '@/api/agent'
// Sample chat data for demonstration
@ -12,6 +12,11 @@ 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 },
})
@ -29,7 +34,7 @@ function handleSend(text: string) {
if (!text.trim())
return
// Add user message
//
const userMessage: ChatMessage = {
id: Date.now(),
content: text,
@ -39,24 +44,121 @@ function handleSend(text: string) {
}
messages.value.push(userMessage)
// Clear input
// AI
const aiMessage: ChatMessage = {
id: Date.now() + 1,
content: '',
sender: 'ai',
timestamp: new Date(),
status: 'sending',
}
messages.value.push(aiMessage)
currentAiMessage.value = aiMessage
//
inputText.value = ''
// Simulate AI response after delay
//
isLoading.value = true
setTimeout(() => {
userMessage.status = 'sent'
isStreaming.value = true
const aiMessage: ChatMessage = {
id: Date.now() + 1,
content: `I received your message: "${text}". This is a simulated response.`,
sender: 'ai',
timestamp: new Date(),
status: 'sent',
// 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)
}
messages.value.push(aiMessage)
isLoading.value = false
}, 1000)
}
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
}
function handleKeydown(e: KeyboardEvent) {

View File

@ -30,8 +30,6 @@ declare module 'vue' {
VanImage: typeof import('vant/es')['Image']
VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar']
VanPicker: typeof import('vant/es')['Picker']
VanPopup: typeof import('vant/es')['Popup']
VanSpace: typeof import('vant/es')['Space']
VanStepper: typeof import('vant/es')['Stepper']
VanSwitch: typeof import('vant/es')['Switch']