feat(llm-chat): 实现流式API响应和打字机效果
添加流式API调用功能,替换原有的模拟响应。实现以下功能: 1. 使用SSE连接处理AI响应 2. 添加打字机效果逐字显示AI回复 3. 增加错误处理和连接管理 4. 移除未使用的Vant组件声明
This commit is contained in:
parent
935c662203
commit
b9d7f495ad
|
|
@ -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
|
||||
inputText.value = ''
|
||||
|
||||
// Simulate AI response after delay
|
||||
isLoading.value = true
|
||||
setTimeout(() => {
|
||||
userMessage.status = 'sent'
|
||||
|
||||
// 创建 AI 消息占位符
|
||||
const aiMessage: ChatMessage = {
|
||||
id: Date.now() + 1,
|
||||
content: `I received your message: "${text}". This is a simulated response.`,
|
||||
content: '',
|
||||
sender: 'ai',
|
||||
timestamp: new Date(),
|
||||
status: 'sent',
|
||||
status: 'sending',
|
||||
}
|
||||
messages.value.push(aiMessage)
|
||||
currentAiMessage.value = aiMessage
|
||||
|
||||
// 清空输入
|
||||
inputText.value = ''
|
||||
|
||||
// 设置加载状态
|
||||
isLoading.value = true
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}, 1000)
|
||||
isStreaming.value = false
|
||||
streamController.value = null
|
||||
currentAiMessage.value = null
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
Loading…
Reference in New Issue