Files
FiDA_Front/src/views/home/agent/components/Agent.vue

287 lines
7.2 KiB
Vue
Raw Normal View History

2026-02-10 13:05:24 +08:00
<template>
<div class="agent-container flex flex-col">
<div class="agent-header flex align-center space-between">
<div class="header-title-wrapper">
<div class="agent-title">{{ props.title }}</div>
<div class="agent-name">AI Assistant 1.0</div>
</div>
<SvgIcon name="equal" color="#0d0d0d" size="24" />
</div>
<div class="agent-body flex-1 flex flex-col">
2026-02-11 16:32:38 +08:00
<List ref="listRef" :message-list="messageList" />
2026-02-10 17:22:40 +08:00
<Input is-agent-mode @send="handleSendMessage" />
2026-02-10 13:05:24 +08:00
</div>
</div>
</template>
<script setup lang="ts">
2026-02-24 13:53:01 +08:00
import { ref, reactive, computed, onUnmounted, onMounted, nextTick } from 'vue'
2026-02-10 13:05:24 +08:00
import List from './List.vue'
import Input from '../../components/Input.vue'
2026-02-11 16:32:38 +08:00
import { fetchAgentReply } from '@/api/agent'
import type { AgentParamsType } from '@/api/agent'
import { useUserInfoStore } from '@/stores'
2026-02-24 13:53:01 +08:00
import { useAgentStore } from '@/stores/agent'
2026-02-10 13:05:24 +08:00
2026-02-11 16:32:38 +08:00
const userStore = useUserInfoStore()
2026-02-24 13:53:01 +08:00
const agentStore = useAgentStore()
2026-02-10 13:05:24 +08:00
const props = withDefaults(
defineProps<{
title: string
}>(),
{
title: 'Retro Sofa Sketch'
}
)
2026-02-11 16:32:38 +08:00
const messageList = ref([])
const listRef = ref()
const params = reactive<AgentParamsType>({
2026-02-24 13:53:01 +08:00
projectID: '',
2026-02-11 16:32:38 +08:00
message: '',
2026-02-24 13:53:01 +08:00
token: userStore.state.token,
versionID: '',
2026-02-11 16:32:38 +08:00
configParams: {
type: 'Chair',
region: 'China',
style: 'Transitional',
temperature: 0.7
2026-02-10 17:22:40 +08:00
},
2026-02-11 16:32:38 +08:00
imageUrlList: []
})
2026-02-10 17:22:40 +08:00
2026-02-11 16:32:38 +08:00
const abort = new AbortController()
onUnmounted(() => {
abort.abort()
})
2026-02-24 13:53:01 +08:00
onMounted(() => {
// 检查 store 中是否有初始项目数据
const initialData = agentStore.getInitialProjectData
if (initialData) {
// 等待页面渲染完成后自动发送初始消息
params.configParams = {
type: initialData.type || 'Chair',
region: initialData.area || 'China',
style: initialData.style || 'Transitional',
temperature: 0.7
}
handleSendMessage({
text: initialData.text,
images: initialData.images
})
// 更新 configParams
// 清空初始数据
agentStore.clearInitialProjectData()
}
})
const handleSendMessage = async (message: {
text: string
images: Array<{ url: string; name: string }>
}) => {
2026-02-10 13:05:24 +08:00
console.log('Message sent:', message)
2026-02-11 16:32:38 +08:00
params.message = message.text
params.imageUrlList = message.images || []
2026-02-10 17:22:40 +08:00
messageList.value.push({
id: messageList.value.length + 1,
2026-02-11 16:32:38 +08:00
text: message.text,
2026-02-10 17:22:40 +08:00
isUser: true
})
// Add AI loading message
const aiMessage = reactive({
id: messageList.value.length + 1,
text: '',
isUser: false,
loading: true,
thinking: false,
thinkingText: '',
thinkingCollapsed: false,
2026-02-11 16:32:38 +08:00
streaming: true
2026-02-10 17:22:40 +08:00
})
messageList.value.push(aiMessage)
2026-02-24 11:17:01 +08:00
// console.log('token---', params.token, '参数---', params)
2026-02-11 16:32:38 +08:00
try {
const urlParams = new URLSearchParams<AgentParamsType>({
...params,
configParams: JSON.stringify(params.configParams)
})
const response = await fetch(`/api/ai-design/chat?${urlParams.toString()}`, {
method: 'GET',
signal: abort.signal
})
// 检查响应内容类型,判断是否为流式响应
const contentType = response.headers.get('content-type') || ''
const isStreamResponse =
contentType.includes('text/event-stream') || contentType.includes('stream')
if (!response.ok) {
// 非流式错误响应,使用 text() 读取错误信息
const errorText = await response.text()
console.error('请求错误:', errorText)
2026-02-23 14:53:29 +08:00
aiMessage.text = '发送失败,请重试'
aiMessage.streaming = false
aiMessage.loading = false
return
2026-02-11 16:32:38 +08:00
}
// 不是流式响应,使用 text()读取错误信息
if (!isStreamResponse) {
const text = await response.text()
try {
const errorData = JSON.parse(text)
console.error('非流式响应:', errorData)
} catch (e) {
console.error('非流式响应文本:', text)
}
2026-02-23 14:53:29 +08:00
aiMessage.text = '发送失败,请重试'
2026-02-11 16:32:38 +08:00
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)
2026-02-10 17:22:40 +08:00
aiMessage.streaming = false
2026-02-11 16:32:38 +08:00
aiMessage.loading = false
flag = false
break
2026-02-10 17:22:40 +08:00
}
2026-02-11 16:32:38 +08:00
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)
2026-02-24 13:53:01 +08:00
// 赋值 project_id 和 version_id
if (jsonData.project_id) params.projectID = jsonData.project_id
if (jsonData.version_id) params.versionID = jsonData.version_id
2026-02-11 16:32:38 +08:00
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)
2026-02-23 14:53:29 +08:00
aiMessage.text = '发送失败,请重试'
2026-02-11 16:32:38 +08:00
aiMessage.streaming = false
aiMessage.loading = false
} finally {
reader.releaseLock()
}
} catch (error) {
console.error('fetch请求失败:', error)
2026-02-23 14:53:29 +08:00
aiMessage.text = '发送失败,请重试'
2026-02-11 16:32:38 +08:00
aiMessage.streaming = false
aiMessage.loading = false
}
2026-02-10 13:05:24 +08:00
}
</script>
<style lang="less" scoped>
.c-svg {
width: initial;
}
.agent-container {
// width: 39%;
width: 63.4rem;
background-color: #fff;
border-radius: 2rem;
box-shadow: 0px 15px 21px 0px #0000000d;
.agent-header {
height: 7.4rem;
border-bottom: 0.1rem solid #c9c9c9;
font-family: 'GeneralMedium';
padding: 1.4rem 3.4rem 1.4rem 3.1rem;
.agent-title {
font-size: 1.8rem;
margin-bottom: 0.6rem;
}
.agent-name {
font-size: 1.4rem;
}
}
.agent-body {
padding: 3.2rem;
2026-02-11 16:32:38 +08:00
overflow: hidden;
2026-02-24 13:53:01 +08:00
row-gap: 2.4rem;
2026-02-10 13:05:24 +08:00
.assist-input-wrapper {
width: 100%;
height: 14.4rem;
min-height: 14.4rem !important;
max-height: 14.4rem !important;
}
}
.input-wrapper {
height: 14.4rem;
padding: 0 2rem 3rem;
}
}
</style>