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">
|
<script setup lang="ts">
|
||||||
import type { ChatMessage } from '@/types/chat'
|
import type { ChatMessage } from '@/types/chat'
|
||||||
import { useScroll } from '@vueuse/core'
|
import { useScroll } from '@vueuse/core'
|
||||||
import { isDark, toggleDark } from '@/composables/dark'
|
import { streamApi } from '@/api/agent'
|
||||||
|
|
||||||
|
|
||||||
// Sample chat data for demonstration
|
// Sample chat data for demonstration
|
||||||
|
|
@ -12,6 +12,11 @@ const inputText = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const messagesContainer = ref<HTMLElement>()
|
const messagesContainer = ref<HTMLElement>()
|
||||||
|
|
||||||
|
// 流式响应相关状态
|
||||||
|
const currentAiMessage = ref<ChatMessage | null>(null)
|
||||||
|
const isStreaming = ref(false)
|
||||||
|
const streamController = ref<AbortController | null>(null)
|
||||||
|
|
||||||
const { arrivedState } = useScroll(messagesContainer, {
|
const { arrivedState } = useScroll(messagesContainer, {
|
||||||
offset: { bottom: 100 },
|
offset: { bottom: 100 },
|
||||||
})
|
})
|
||||||
|
|
@ -29,7 +34,7 @@ function handleSend(text: string) {
|
||||||
if (!text.trim())
|
if (!text.trim())
|
||||||
return
|
return
|
||||||
|
|
||||||
// Add user message
|
// 添加用户消息
|
||||||
const userMessage: ChatMessage = {
|
const userMessage: ChatMessage = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
content: text,
|
content: text,
|
||||||
|
|
@ -39,24 +44,121 @@ function handleSend(text: string) {
|
||||||
}
|
}
|
||||||
messages.value.push(userMessage)
|
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 = ''
|
inputText.value = ''
|
||||||
|
|
||||||
// Simulate AI response after delay
|
// 设置加载状态
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
setTimeout(() => {
|
isStreaming.value = true
|
||||||
userMessage.status = 'sent'
|
|
||||||
|
|
||||||
const aiMessage: ChatMessage = {
|
// 创建 AbortController(虽然用户不需要中止功能,但保留以备后用)
|
||||||
id: Date.now() + 1,
|
const controller = new AbortController()
|
||||||
content: `I received your message: "${text}". This is a simulated response.`,
|
streamController.value = controller
|
||||||
sender: 'ai',
|
|
||||||
timestamp: new Date(),
|
// 调用流式 API
|
||||||
status: 'sent',
|
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) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ declare module 'vue' {
|
||||||
VanImage: typeof import('vant/es')['Image']
|
VanImage: typeof import('vant/es')['Image']
|
||||||
VanList: typeof import('vant/es')['List']
|
VanList: typeof import('vant/es')['List']
|
||||||
VanNavBar: typeof import('vant/es')['NavBar']
|
VanNavBar: typeof import('vant/es')['NavBar']
|
||||||
VanPicker: typeof import('vant/es')['Picker']
|
|
||||||
VanPopup: typeof import('vant/es')['Popup']
|
|
||||||
VanSpace: typeof import('vant/es')['Space']
|
VanSpace: typeof import('vant/es')['Space']
|
||||||
VanStepper: typeof import('vant/es')['Stepper']
|
VanStepper: typeof import('vant/es')['Stepper']
|
||||||
VanSwitch: typeof import('vant/es')['Switch']
|
VanSwitch: typeof import('vant/es')['Switch']
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue