feat: 对接AI对话接口

This commit is contained in:
2026-02-11 16:32:38 +08:00
parent 0d13d69339
commit 7656250d9a
7 changed files with 637 additions and 95 deletions

View File

@@ -8,17 +8,21 @@
<SvgIcon name="equal" color="#0d0d0d" size="24" />
</div>
<div class="agent-body flex-1 flex flex-col">
<List :message-list="messageList" />
<List ref="listRef" :message-list="messageList" />
<Input is-agent-mode @send="handleSendMessage" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ref, reactive, computed, onUnmounted } from 'vue'
import List from './List.vue'
import Input from '../../components/Input.vue'
import { fetchAgentReply } from '@/api/agent'
import type { AgentParamsType } from '@/api/agent'
import { useUserInfoStore } from '@/stores'
const userStore = useUserInfoStore()
const props = withDefaults(
defineProps<{
title: string
@@ -28,24 +32,35 @@
}
)
const messageList = ref([
{ id: 1, text: 'Hello', isUser: true },
{
id: 2,
text: 'Hey, I am your design assistant FiDA. I noticed that you want to design a yellow sofa. I can help you! Tell me what else you need?'
const messageList = ref([])
const listRef = ref()
const params = reactive<AgentParamsType>({
threadId: '',
message: '',
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzcwNzkxMzEyLCJleHAiOjE3NzA4Nzc3MTJ9.xydPinm9l5Yq6GMkfaaVvdHjiINaYrp5VkRM7B9g83A',
checkpointId: '',
configParams: {
type: 'Chair',
region: 'China',
style: 'Transitional',
temperature: 0.7
},
{
id: 3,
text: 'Please design a vintage-inspired sofa with smooth, flowing lines and a sculptural silhouette. The sofa features a retro aesthetic combined with elegant curves, creating a timeless and refined look.',
isUser: true
}
])
imageUrlList: []
})
const handleSendMessage = (message: string) => {
const abort = new AbortController()
onUnmounted(() => {
abort.abort()
})
const handleSendMessage = async (message: string) => {
console.log('Message sent:', message)
params.message = message.text
params.imageUrlList = message.images || []
messageList.value.push({
id: messageList.value.length + 1,
text: message,
text: message.text,
isUser: true
})
@@ -58,44 +73,138 @@
thinking: false,
thinkingText: '',
thinkingCollapsed: false,
streaming: false
streaming: true
})
messageList.value.push(aiMessage)
// Simulate AI response
setTimeout(() => {
aiMessage.loading = false
aiMessage.thinking = true
aiMessage.thinkingText = '思考中...'
const threadId = '' //
console.log('token---', params.token, '参数---', params)
// Simulate thinking process
setTimeout(() => {
aiMessage.thinkingText = '分析用户需求:设计复古风格沙发,强调流畅线条和雕塑轮廓。'
}, 1000)
try {
const urlParams = new URLSearchParams<AgentParamsType>({
...params,
configParams: JSON.stringify(params.configParams)
})
setTimeout(() => {
aiMessage.thinkingText += '\n考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素考虑颜色方案黄色基调结合复古元素。'
}, 2000)
const response = await fetch(`/api/ai-design/chat?${urlParams.toString()}`, {
method: 'GET',
signal: abort.signal
})
setTimeout(() => {
aiMessage.thinkingText += '\n生成设计草图...'
aiMessage.thinking = false
aiMessage.streaming = true
// 检查响应内容类型,判断是否为流式响应
const contentType = response.headers.get('content-type') || ''
const isStreamResponse =
contentType.includes('text/event-stream') || contentType.includes('stream')
// Simulate streaming response
const response = '根据您的描述,我为您设计了一个复古风格的沙发。这个沙发采用黄色皮革材质,线条流畅,轮廓雕塑感强。整体造型优雅,结合了现代舒适与复古美学。'
let index = 0
const interval = setInterval(() => {
if (index < response.length) {
aiMessage.text += response[index]
index++
} else {
clearInterval(interval)
if (!response.ok) {
// 非流式错误响应,使用 text() 读取错误信息
const errorText = await response.text()
console.error('请求错误:', errorText)
throw new Error(`发起对话错误--- ${response.status}: ${errorText}`)
}
// 不是流式响应,使用 text()读取错误信息
if (!isStreamResponse) {
const text = await response.text()
try {
const errorData = JSON.parse(text)
console.error('非流式响应:', errorData)
} catch (e) {
console.error('非流式响应文本:', text)
}
aiMessage.streaming = false
aiMessage.loading = false
return
}
// 流式响应处理
let contentBody = ''
let buffer = ''
const reader = response.body?.getReader()
if (!reader) throw new Error('无法获取流读取器')
const decoder = new TextDecoder()
try {
let flag = true
while (flag) {
const { done, value } = await reader.read()
if (done) {
console.log('传输结束 end---', contentBody)
aiMessage.streaming = false
aiMessage.loading = false
flag = false
break
}
}, 50)
}, 3000)
}, 1000)
buffer += decoder.decode(value, { stream: true })
// 优先按空行拆分事件块SSE标准
let events = buffer.split(/\n\n/)
buffer = events.pop() // 保留不完整块
for (let event of events) {
if (!event.trim()) continue
// 过滤掉 id: 等字段,只取 data:
const dataLines = event
.split(/\n/)
.filter((line) => line.startsWith('data:'))
.map((line) => line.replace(/^data:\s*/, '').trim())
if (dataLines.length === 0) continue
const jsonText = dataLines.join('\n')
try {
const jsonData = JSON.parse(jsonText)
// 赋值 thread_id 和 checkpoint_id
if (jsonData.thread_id) params.threadId = jsonData.thread_id
if (jsonData.checkpoint_id) params.checkpointId = jsonData.checkpoint_id
if (
jsonData.content &&
jsonData.content.length > 0 &&
jsonData.type !== 'end'
) {
contentBody += jsonData.content
aiMessage.text = contentBody
}
if (jsonData.type === 'end') {
aiMessage.streaming = false
aiMessage.loading = false
flag = false
break
}
} catch (e) {
// 检查是否为纯文本 [DONE]
if (jsonText.trim() === '[DONE]') {
console.log('结束-----------------------')
aiMessage.streaming = false
aiMessage.loading = false
flag = false
break
}
// JSON 不完整:保留到下一次循环
if (!jsonText.trim().endsWith('}')) {
buffer = 'data: ' + jsonText // 重新放回缓存
continue
} else {
console.warn('⚠️ JSON 格式错误,跳过:', jsonText)
}
}
}
}
} catch (error) {
console.error('流式传输错误:', error)
aiMessage.streaming = false
aiMessage.loading = false
} finally {
reader.releaseLock()
}
} catch (error) {
console.error('fetch请求失败:', error)
aiMessage.streaming = false
aiMessage.loading = false
}
}
</script>
@@ -127,6 +236,8 @@
}
.agent-body {
padding: 3.2rem;
overflow: hidden;
row-gap: 2.4rem;
.assist-input-wrapper {
width: 100%;
height: 14.4rem;