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

625 lines
16 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" /> -->
2026-02-10 13:05:24 +08:00
</div>
<div class="agent-body flex-1 flex flex-col">
2026-02-25 11:00:31 +08:00
<List ref="listRef" :message-list="messageList" @regenerate="handleRegenerate" />
<Input
2026-02-26 16:48:08 +08:00
ref="inputRef"
2026-02-25 11:00:31 +08:00
is-agent-mode
:generating="isGenerating"
@send="handleSendMessage"
@pause="handlePause"
/>
2026-02-10 13:05:24 +08:00
</div>
</div>
</template>
<script setup lang="ts">
2026-02-26 16:48:08 +08:00
import { ref, reactive, computed, onUnmounted, onMounted, nextTick, watch } from 'vue'
2026-02-10 13:05:24 +08:00
import List from './List.vue'
import Input from '../../components/Input.vue'
import { chatUrl } from '@/api/agent'
2026-02-11 16:32:38 +08:00
import type { AgentParamsType } from '@/api/agent'
import { useUserInfoStore, useProjectStore, useAgentStore } from '@/stores'
2026-03-11 14:32:13 +08:00
import MyEvent from '@/utils/myEvent'
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-24 14:28:25 +08:00
const projectStore = useProjectStore()
2026-02-25 15:10:03 +08:00
2026-03-13 17:23:56 +08:00
const reportsContent = ref('')
const isGeneratingReport = ref(false)
const emits = defineEmits(['update:sketchList', 'setTitle'])
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([])
2026-02-25 15:10:03 +08:00
2026-02-11 16:32:38 +08:00
const listRef = ref()
2026-02-26 16:48:08 +08:00
const inputRef = ref()
2026-02-25 11:00:31 +08:00
const isGenerating = ref(false)
const isPaused = ref(false) // 标记是否为主动暂停
2026-02-11 16:32:38 +08:00
const params = reactive<AgentParamsType>({
2026-02-25 11:00:31 +08:00
projectID: projectStore.state.id,
2026-02-11 16:32:38 +08:00
message: '',
2026-02-24 13:53:01 +08:00
token: userStore.state.token,
versionID: '',
needSuggestion: false,
useReport: false,
2026-02-11 16:32:38 +08:00
configParams: {
type: '',
region: '',
style: ''
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-25 15:10:03 +08:00
const sketchList = ref([])
2026-02-26 16:48:08 +08:00
watch(
sketchList,
(newVal) => {
emits('update:sketchList', newVal)
},
{ deep: true }
)
2026-02-25 15:10:03 +08:00
2026-02-25 11:00:31 +08:00
// 每次请求时创建新的 AbortController
let abort: AbortController
const createAbortController = () => {
// 如果已有未完成的中止,先中止它
if (abort) {
abort.abort()
}
abort = new AbortController()
return abort
}
2026-02-11 16:32:38 +08:00
onUnmounted(() => {
2026-02-25 11:00:31 +08:00
abort?.abort()
2026-02-11 16:32:38 +08:00
})
2026-02-24 13:53:01 +08:00
onMounted(() => {
// 检查 store 中是否有初始项目数据
2026-02-25 11:00:31 +08:00
// projectStore.setId('1') // 临时设置项目ID为1实际应用中应根据上下文动态设置
2026-02-24 13:53:01 +08:00
const initialData = agentStore.getInitialProjectData
if (initialData) {
// 等待页面渲染完成后自动发送初始消息
params.configParams = {
2026-02-25 15:10:03 +08:00
type: initialData.type,
2026-02-25 11:00:31 +08:00
region: initialData.area,
2026-02-25 15:10:03 +08:00
style: initialData.style,
2026-02-24 13:53:01 +08:00
temperature: 0.7
}
params.needSuggestion = initialData.needSuggestion || false
params.useReport = initialData.useReport
2026-02-24 13:53:01 +08:00
handleSendMessage({
text: initialData.text,
2026-03-03 14:40:49 +08:00
images: initialData.images,
tempImages: initialData.tempImages
2026-02-24 13:53:01 +08:00
})
// 更新 configParams
// 清空初始数据
agentStore.clearInitialProjectData()
}
})
2026-02-25 15:10:03 +08:00
const handleSendMessage = async (
message: {
text: string
images: Array<{ url: string; name: string }>
2026-03-02 15:08:03 +08:00
tempImages: any[]
2026-02-25 15:10:03 +08:00
},
skipUserMessage = false
) => {
2026-02-25 11:00:31 +08:00
isPaused.value = false
isGenerating.value = true
2026-02-11 16:32:38 +08:00
params.message = message.text
params.imageUrlList = message.images || []
2026-02-25 11:00:31 +08:00
// 如果不是重新生成模式,则添加用户消息到列表
if (!skipUserMessage) {
messageList.value.push({
id: messageList.value.length + 1,
text: message.text,
2026-03-02 15:08:03 +08:00
isUser: true,
imageUrls: message.tempImages
2026-02-25 11:00:31 +08:00
})
}
2026-02-10 17:22:40 +08:00
// 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-25 11:00:31 +08:00
// 创建新的 AbortController
const abortController = createAbortController()
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)
})
2026-03-02 15:41:24 +08:00
const BASEURL = import.meta.env.VITE_APP_URL
const response = await fetch(`${BASEURL}${chatUrl}?${urlParams.toString()}`, {
2026-02-11 16:32:38 +08:00
method: 'GET',
2026-02-25 11:00:31 +08:00
signal: abortController.signal
2026-02-11 16:32:38 +08:00
})
// 检查响应内容类型,判断是否为流式响应
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-25 11:00:31 +08:00
if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
2026-02-23 14:53:29 +08:00
aiMessage.streaming = false
aiMessage.loading = false
2026-02-25 11:00:31 +08:00
isGenerating.value = false
2026-02-23 14:53:29 +08:00
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-25 11:00:31 +08:00
if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
2026-02-11 16:32:38 +08:00
aiMessage.streaming = false
aiMessage.loading = false
2026-02-25 11:00:31 +08:00
isGenerating.value = false
2026-02-11 16:32:38 +08:00
return
}
// 流式响应处理
let contentBody = ''
let buffer = ''
2026-03-13 17:23:56 +08:00
const webAddressList = []
2026-02-11 16:32:38 +08:00
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) {
2026-03-02 11:29:07 +08:00
// 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
2026-02-25 15:10:03 +08:00
isGenerating.value = false
2026-02-11 16:32:38 +08:00
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 })
2026-02-11 16:32:38 +08:00
// 优先按空行拆分事件块SSE标准
let events = buffer.split(/\n\n/)
buffer = events.pop() // 保留不完整块
2026-03-13 17:23:56 +08:00
let previousEventName = '' // 记录上一个事件名称
let hasReportStarted = false // 标记 report 是否已经开始
2026-02-11 16:32:38 +08:00
for (let event of events) {
if (!event.trim()) continue
2026-03-13 17:23:56 +08:00
// 解析事件名称(从 event:xxx 行)
const eventName =
event
.split(/\n/)
.find((line) => line.startsWith('event:'))
?.replace(/^event:\s*/, '')
?.trim() || ''
if (!hasReportStarted && eventName === 'report') {
console.log('开始生成报告--------')
isGeneratingReport.value = true
contentBody += `<slot slot-name="card" title="123">123</slot>`
hasReportStarted = true
}
if (
previousEventName === 'report' &&
eventName !== 'report' &&
reportsContent.value
) {
isGeneratingReport.value = false
localStorage.setItem('reportsContent', reportsContent.value)
2026-02-24 14:24:45 +08:00
}
2026-03-13 17:23:56 +08:00
previousEventName = eventName
// console.log('eventName:', eventName, 'event:', event)
// 根据事件名称精确判断,而不是用 includes
if (eventName === 'error') {
2026-02-25 15:10:03 +08:00
aiMessage.text = '出现错误,请重试'
aiMessage.streaming = false
aiMessage.loading = false
isGenerating.value = false
flag = false
break
}
2026-03-13 17:23:56 +08:00
if (eventName === 'todo') {
2026-03-11 14:32:13 +08:00
break
}
2026-03-13 17:23:56 +08:00
let isNodeIdEvent = eventName === 'nodeId'
let hasSketch = eventName === 'sketch'
2026-02-25 11:00:31 +08:00
const dataLines = event
.split(/\n/)
.filter((line) => line.startsWith('data:'))
2026-03-13 17:23:56 +08:00
.map((line) => line.replace(/^data:\s*/, ''))
.filter((content) => content.startsWith('{') || content.startsWith('['))
2026-03-13 17:23:56 +08:00
// console.log('dataLInes', dataLines)
2026-02-25 11:00:31 +08:00
if (isNodeIdEvent) {
2026-02-24 14:24:45 +08:00
params.versionID = dataLines[0]
2026-03-02 13:54:52 +08:00
projectStore.setProject({ nodeId: dataLines[0] })
2026-02-24 14:24:45 +08:00
}
2026-03-13 17:23:56 +08:00
if (eventName === 'webAddress') {
console.log('webAddress-----', eventName, dataLines)
}
2026-03-13 17:23:56 +08:00
if (eventName === 'tool') {
2026-03-11 14:32:13 +08:00
MyEvent.emit('loading-sketch')
}
2026-02-25 11:00:31 +08:00
2026-02-11 16:32:38 +08:00
if (dataLines.length === 0) continue
const jsonText = dataLines.join('\n')
try {
const jsonData = JSON.parse(jsonText)
2026-03-13 17:23:56 +08:00
// console.log('jsonData', jsonData)
if (jsonData.webAddress) {
console.log('webAddress-----', jsonData)
}
if (jsonData.title) {
emits('setTitle', jsonData.title)
}
2026-02-26 16:48:08 +08:00
if (hasSketch) {
sketchList.value.push({
[Object.keys(jsonData)[0]]: jsonData[Object.keys(jsonData)[0]]
})
2026-02-25 15:10:03 +08:00
}
2026-03-13 17:23:56 +08:00
if (eventName === 'report') {
reportsContent.value += jsonData.content
} else {
if (jsonData.reasoning) {
aiMessage.thinking = true
aiMessage.loading = false
aiMessage.thinkingText += jsonData.reasoning
} else {
aiMessage.thinking = false
if (
jsonData.content &&
jsonData.content.length > 0 &&
jsonData.type !== 'end'
) {
contentBody += jsonData.content
aiMessage.text = contentBody
aiMessage.loading = false
}
}
2026-02-11 16:32:38 +08:00
}
if (jsonData.type === 'end') {
aiMessage.streaming = false
aiMessage.loading = false
2026-02-25 11:00:31 +08:00
isGenerating.value = false
2026-02-11 16:32:38 +08:00
flag = false
break
}
} catch (e) {
// 检查是否为纯文本 [DONE]
if (jsonText.trim() === '[DONE]') {
console.log('结束-----------------------')
aiMessage.streaming = false
aiMessage.loading = false
2026-02-25 11:00:31 +08:00
isGenerating.value = false
2026-02-11 16:32:38 +08:00
flag = false
break
}
// JSON 不完整:保留到下一次循环
if (!jsonText.trim().endsWith('}')) {
buffer = 'data: ' + jsonText // 重新放回缓存
continue
} else {
console.warn('⚠️ JSON 格式错误,跳过:', jsonText)
}
}
2026-03-13 17:23:56 +08:00
// 更新上一个事件名称
previousEventName = eventName
2026-02-11 16:32:38 +08:00
}
}
} catch (error) {
console.error('流式传输错误:', error)
2026-02-25 11:00:31 +08:00
if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
2026-02-11 16:32:38 +08:00
aiMessage.streaming = false
aiMessage.loading = false
2026-02-25 11:00:31 +08:00
isGenerating.value = false
2026-02-11 16:32:38 +08:00
} finally {
reader.releaseLock()
}
} catch (error) {
console.error('fetch请求失败:', error)
2026-02-25 11:00:31 +08:00
if (!isPaused.value) {
aiMessage.text = '发送失败,请重试'
}
2026-02-11 16:32:38 +08:00
aiMessage.streaming = false
aiMessage.loading = false
2026-02-25 11:00:31 +08:00
isGenerating.value = false
}
}
const handlePause = () => {
isPaused.value = true
isGenerating.value = false
abort?.abort()
}
const handleRegenerate = async (aiMessage: any) => {
// 找到当前 AI 消息在列表中的索引
const aiIndex = messageList.value.findIndex((msg) => msg.id === aiMessage.id)
if (aiIndex === -1) return
// 找到对应的用户消息AI 消息前面的最近一条用户消息)
let userMessage = null
for (let i = aiIndex - 1; i >= 0; i--) {
if (messageList.value[i].isUser) {
userMessage = messageList.value[i]
break
}
2026-02-11 16:32:38 +08:00
}
2026-02-25 11:00:31 +08:00
if (!userMessage) return
// 删除当前的 AI 回复消息
messageList.value.splice(aiIndex, 1)
// 重新调用 API跳过用户消息添加因为用户消息已存在
await handleSendMessage(
{
text: userMessage.text,
images: userMessage.images || []
},
true
)
2026-02-10 13:05:24 +08:00
}
2026-02-26 16:48:08 +08:00
2026-03-04 14:02:11 +08:00
// 处理对话列表,将连续的 assistant 消息合并为一条
2026-03-13 17:23:56 +08:00
const processDialogue = (dialogue, startIndex, existingImgList, sessionId) => {
2026-03-04 14:02:11 +08:00
if (!dialogue || dialogue.length === 0) return []
const result = []
let i = startIndex
while (i < dialogue.length) {
const item = dialogue[i]
if (item.role === 'user') {
// user 角色直接添加
result.push({
...item,
text: item.content,
isUser: true,
2026-03-13 17:23:56 +08:00
id: result.length + 1,
sessionId: sessionId
2026-03-04 14:02:11 +08:00
})
i++
} else if (item.role === 'assistant') {
// assistant 角色,拼接直到下一个 user
let combinedContent = item.content || ''
2026-03-13 17:23:56 +08:00
let combinedThinkingText = item.reasoning || ''
2026-03-04 14:02:11 +08:00
// 继续往后找连续的 assistant 消息
let j = i + 1
while (j < dialogue.length && dialogue[j].role === 'assistant') {
combinedContent += dialogue[j].content || ''
2026-03-13 17:23:56 +08:00
combinedThinkingText += dialogue[j].reasoning || ''
2026-03-04 14:02:11 +08:00
j++
}
result.push({
...item,
content: combinedContent,
2026-03-13 17:23:56 +08:00
thinkingText: combinedThinkingText,
2026-03-04 14:02:11 +08:00
text: combinedContent,
isUser: false,
2026-03-13 17:23:56 +08:00
id: result.length + 1,
sessionId: sessionId
2026-03-04 14:02:11 +08:00
})
i = j
} else {
// 其他角色直接添加
result.push({
...item,
text: item.content,
isUser: item.role === 'user',
2026-03-13 17:23:56 +08:00
id: result.length + 1,
sessionId: sessionId
2026-03-04 14:02:11 +08:00
})
i++
}
}
return result
}
2026-03-13 17:23:56 +08:00
const processSession = (session, imgList, ancestorsList, idCounterRef) => {
if (!session) return
// 1. 在 dialogue 第一个 report 有值的项的 content 中插入报告卡片
if (session.dialogue) {
for (let i = 0; i < session.dialogue.length; i++) {
if (session.dialogue[i].report) {
session.dialogue[i].content =
`<slot slot-name="card" title="123">123</slot>` +
(session.dialogue[i].content || '')
break
}
}
}
// 2. 收集 report 内容并保存到 localStorage
let reportStr = ''
session.dialogue?.forEach((item) => {
if (item.report) {
reportStr += item.report
}
})
if (reportStr && session.id) {
localStorage.setItem(`reportsContent_${session.id}`, reportStr)
}
// 3. 收集 sketchIDAndUrl 到 imgList
if (session.sketchIDAndUrl) {
imgList.push(...session.sketchIDAndUrl)
}
// 4. 处理 dialogue
const list = processDialogue(session.dialogue, 0, imgList, session.id)
list.forEach((el) => {
el.id = idCounterRef.value++
})
ancestorsList.push(...list)
}
2026-03-04 14:02:11 +08:00
const setChatInfo = (info) => {
2026-03-03 09:59:19 +08:00
const initialData = agentStore.getInitialProjectData
if (isGenerating.value || initialData) return
const data = info.conversation
let project = info.project
if (info.id) {
project = info
}
params.versionID = ''
sketchList.value = []
if (project) {
params.configParams.type = project.type
params.configParams.region = project.area
params.configParams.style = project.style
params.configParams.temperature = project.temperature
}
// 如果没有数据,直接返回
2026-03-03 09:59:19 +08:00
if (!data) {
messageList.value = []
return
}
2026-03-02 11:29:07 +08:00
const { ancestors, current } = data
2026-03-03 10:28:16 +08:00
2026-03-13 17:23:56 +08:00
// 处理单个会话ancestor 或 current
const imgList = []
2026-03-02 11:29:07 +08:00
const ancestorsList = []
2026-03-13 17:23:56 +08:00
const idCounterRef = { value: 1 }
2026-03-11 10:53:25 +08:00
2026-03-13 17:23:56 +08:00
// 处理所有 ancestors
ancestors?.forEach((item) => {
processSession(item, imgList, ancestorsList, idCounterRef)
2026-03-04 14:02:11 +08:00
})
2026-03-02 13:54:52 +08:00
2026-03-13 17:23:56 +08:00
// 处理 current
processSession(current, imgList, ancestorsList, idCounterRef)
// 延迟设置新数据,确保 UI 有时间响应清空操作
nextTick(() => {
2026-03-13 17:23:56 +08:00
messageList.value = [...ancestorsList]
params.versionID = current?.id
sketchList.value = imgList
})
2026-03-02 11:29:07 +08:00
}
2026-02-26 16:48:08 +08:00
defineExpose({
2026-03-02 11:29:07 +08:00
setChatInfo
2026-02-26 16:48:08 +08:00
})
2026-02-10 13:05:24 +08:00
</script>
<style lang="less" scoped>
.c-svg {
width: initial;
}
.agent-container {
// width: 39%;
2026-02-25 15:12:04 +08:00
// width: 63.4rem;
// flex: 1;
2026-03-06 13:26:14 +08:00
width: 67.4rem;
2026-02-25 15:12:04 +08:00
margin-right: 2.7rem;
2026-02-10 13:05:24 +08:00
background-color: #fff;
border-radius: 2rem;
box-shadow: 0px 15px 21px 0px #0000000d;
.agent-header {
height: 7.4rem;
border-bottom: 0.1rem solid #c9c9c9;
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%;
2026-03-06 13:26:14 +08:00
// height: 14.4rem;
2026-02-10 13:05:24 +08:00
min-height: 14.4rem !important;
2026-03-06 13:26:14 +08:00
max-height: 18.1rem !important;
2026-02-10 13:05:24 +08:00
}
}
.input-wrapper {
height: 14.4rem;
padding: 0 2rem 3rem;
}
}
</style>