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-11 16:32:38 +08:00
|
|
|
|
import { ref, reactive, computed, onUnmounted } 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-10 13:05:24 +08:00
|
|
|
|
|
2026-02-11 16:32:38 +08:00
|
|
|
|
const userStore = useUserInfoStore()
|
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>({
|
|
|
|
|
|
threadId: '',
|
|
|
|
|
|
message: '',
|
|
|
|
|
|
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwiaWF0IjoxNzcwNzkxMzEyLCJleHAiOjE3NzA4Nzc3MTJ9.xydPinm9l5Yq6GMkfaaVvdHjiINaYrp5VkRM7B9g83A',
|
|
|
|
|
|
checkpointId: '',
|
|
|
|
|
|
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()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleSendMessage = async (message: 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
|
|
|
|
// const threadId = '' //
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
// 赋值 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)
|
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;
|
|
|
|
|
|
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>
|