Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
X1627315083@163.com
2026-02-13 13:04:07 +08:00
25 changed files with 1082 additions and 110 deletions

View File

@@ -8,16 +8,21 @@
<SvgIcon name="equal" color="#0d0d0d" size="24" />
</div>
<div class="agent-body flex-1 flex flex-col">
<List />
<Input :is-agent-mode="true" @send="handleSendMessage" />
<List ref="listRef" :message-list="messageList" />
<Input is-agent-mode @send="handleSendMessage" />
</div>
</div>
</template>
<script setup lang="ts">
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
@@ -27,8 +32,179 @@
}
)
const handleSendMessage = (message: string) => {
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
},
imageUrlList: []
})
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,
isUser: true
})
// Add AI loading message
const aiMessage = reactive({
id: messageList.value.length + 1,
text: '',
isUser: false,
loading: true,
thinking: false,
thinkingText: '',
thinkingCollapsed: false,
streaming: true
})
messageList.value.push(aiMessage)
const threadId = '' //
console.log('token---', params.token, '参数---', params)
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)
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
}
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>
@@ -60,6 +236,8 @@
}
.agent-body {
padding: 3.2rem;
overflow: hidden;
row-gap: 2.4rem;
.assist-input-wrapper {
width: 100%;
height: 14.4rem;

View File

@@ -1,11 +1,16 @@
<template>
<div class="agent-item">
<div class="message-wrapper flex align-center" :class="{ 'is-user': content.isUser }">
<div class="message-wrapper flex" :class="{ 'is-user': content.isUser }">
<div class="thumb">
<img :src="content.isUser ? userThumb : agentThumb" class="thumb-icon" />
</div>
<div class="message-context">
<div class="message-txt">{{ content.text }}</div>
<div
class="message-context"
v-show="!content.loading && !content.thinking && !content.streaming"
>
<div class="message-txt markdown-body">
<div v-html="formatMessage"></div>
</div>
<div class="operate flex" :class="{ 'is-user': content.isUser }">
<template v-if="content.isUser">
<SvgIcon name="copy" size="16" color="#000" @click.stop="handleCopyText" />
@@ -22,15 +27,38 @@
</template>
</div>
</div>
<div class="message-context" v-show="content.loading">
<div class="generating">Generating...</div>
</div>
<div class="message-context" v-show="content.thinking">
<div class="thinking">
<div class="thinking-header flex align-center" @click="toggleThinkingCollapsed">
<span>思考中</span>
<!-- <SvgIcon :name="content.thinkingCollapsed ? 'arrowDown' : 'arrowUp'" size="16" color="#666" /> -->
</div>
<div class="thinking-content" v-show="!content.thinkingCollapsed">
<pre>{{ content.thinkingText }}</pre>
</div>
</div>
</div>
<div class="message-context" v-show="content.streaming">
<div class="message-txt">
<div v-html="formatMessage"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import gsap from 'gsap'
import userThumb from '@/assets/images/user-thumb.jpg'
import agentThumb from '@/assets/images/agent-thumb.jpg'
import markdownIt from 'markdown-it'
const md = new markdownIt()
const { t } = useI18n()
@@ -38,6 +66,21 @@
content: Object
}>()
const formatMessage = computed(() => {
// MARKDOWN.renderer.rules.link_open = (tokens, idx, options, env, self) => {
// const aIndex = tokens[idx].attrIndex('target')
// if (aIndex < 0) {
// tokens[idx].attrPush(['target', '_blank'])
// } else {
// tokens[idx].attrs[aIndex][1] = '_blank'
// }
// return self.renderToken(tokens, idx, options, env, self)
// }
const str = md.render(props.content.text)
console.log('str',str)
return str
})
const operateList = ref([
{
name: 'thumbUp',
@@ -65,6 +108,8 @@
}
])
const loading = ref(false)
const handleCopyText = () => {
navigator.clipboard
.writeText(props.content.text)
@@ -85,6 +130,10 @@
})
})
}
const toggleThinkingCollapsed = () => {
props.content.thinkingCollapsed = !props.content.thinkingCollapsed
}
</script>
<style lang="less" scoped>
@@ -97,6 +146,7 @@
font-size: 1.4rem;
.message-wrapper {
column-gap: 0.9rem;
// align-items: flex-start;
&.is-user {
text-align: right;
flex-direction: row-reverse;
@@ -115,6 +165,10 @@
border-radius: 50%;
}
}
.message-context{
line-height: 2rem;
font-size: 1.4rem;
}
}
.operate {
margin-top: 1.3rem;
@@ -124,4 +178,38 @@
}
}
}
.generating {
font-family: 'GeneralBold';
font-weight: 600;
font-size: 1.55rem;
background: linear-gradient(45deg, #f2ab4a, #ff6b75, #fe3b55);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.thinking {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
background-color: #f9f9f9;
.thinking-header {
cursor: pointer;
font-weight: bold;
justify-content: space-between;
}
.thinking-content {
margin-top: 0.5rem;
pre {
white-space: pre-wrap;
font-family: inherit;
font-size: 1.2rem;
color: #666;
}
}
}
</style>

View File

@@ -1,29 +1,41 @@
<template>
<div class="agent-list flex flex-col flex-1">
<div class="agent-list flex flex-col flex-1" ref="listContainer">
<Item v-for="message in messageList" :key="message.id" :content="message" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, nextTick, watch } from 'vue'
import Item from './Item.vue'
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?'
},
{
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
}
])
const props = defineProps<{
messageList: Array<any>
}>()
const listContainer = ref<HTMLDivElement>()
const scrollToBottom = () => {
nextTick(() => {
if (listContainer.value) {
listContainer.value.scrollTop = listContainer.value.scrollHeight
}
})
}
watch(() => props.messageList, () => {
scrollToBottom()
}, { deep: true })
defineExpose({ scrollToBottom })
</script>
<style lang="less" scoped>
.agent-list {
row-gap: 3.2rem;
overflow-y: auto;
// 隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<div class="menu-wrapper flex space-between">
<div class="menu-item"></div>
<div class="menu-item"></div>
<div class="menu-item"></div>
</div>
</template>
<script setup></script>
<style lang="less" scoped>
.menu-wrapper {
width: 1.8rem;
cursor: pointer;
.menu-item {
width: 0.4rem;
height: 0.4rem;
background-color: #000;
border-radius: 50%;
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div
class="preview-container flex"
:class="type === 'sketch' ? 'sketch-preview' : 'report-preview'"
>
<template v-if="type === 'sketch'">
<div class="sketch-item" v-for="item in 12">
<Menu class="menu-btn" @click="handleClickMenu" />
<div class="edit-btn flex align-center space-between" @click="handleClickEdit">
<div>Edit</div>
<img src="@/assets/images/arrow-top-right.png" alt="" />
</div>
<img :src="LoadingImg" alt="" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Menu from './Menu.vue'
import LoadingImg from '@/assets/images/sketch-loading.gif'
const props = withDefaults(
defineProps<{
type: 'sketch' | 'report'
}>(),
{
type: 'sketch'
}
)
const handleClickEdit = () => {
// 编辑按钮点击逻辑
console.log('Edit button clicked')
}
const handleClickMenu = () => {
// 菜单按钮点击逻辑
console.log('Menu button clicked')
}
</script>
<style lang="less" scoped>
.preview-container {
gap: 1.2rem;
flex-wrap: wrap;
.sketch-item {
position: relative;
&,
img {
width: 21.9rem;
height: 21.9rem;
border-radius: 1.6rem;
}
.menu-btn {
position: absolute;
top: 2.1rem;
right: 1.5rem;
}
.edit-btn {
position: absolute;
right: 1rem;
bottom: 1.1rem;
width: 7.8rem;
height: 3.59rem;
border-radius: 2rem;
background-color: #fff;
border: 0.2rem solid #e5e5e5;
font-family: 'GeneralMedium';
font-size: 1.4rem;
padding: 0 0.9rem 0 1.4rem;
cursor: pointer;
img{
width: 1.8rem;
height: 1.8rem;
}
}
}
}
</style>

View File

@@ -1,14 +1,19 @@
<template>
<div class="agent-wrapper flex space-between">
<Agent :title="agentTitle" />
<div class="preview-wrapper">preview</div>
<div class="preview-wrapper">
<Preview :type="previewType" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Agent from './components/Agent.vue'
import Preview from './components/Preview.vue'
const agentTitle = ref('Retro Sofa Sketch')
const previewType = ref<'sketch' | 'report'>('sketch')
</script>
<style lang="less" scoped>
@@ -24,7 +29,6 @@
.preview-wrapper {
width: 91.2rem;
background-color: #f5f5f5;
}
}
</style>

View File

@@ -22,6 +22,7 @@
:placeholder="$t('Input.placeholder')"
@input="handleEditorInput"
@paste="handleEditorPaste"
@keypress="handleKeyPress"
>
<!-- <Tag v-if="showReportTag" /> -->
<div
@@ -188,7 +189,7 @@
}
)
const emits = defineEmits(['send'])
const emits = defineEmits(['send'])
const { t } = useI18n()
@@ -295,14 +296,23 @@
}
}
const handleSendAgent=()=>{
emits('send', inputValue.value)
// 发送后清空输入框
if(editorRef.value){
editorRef.value.innerHTML = ''
}
inputValue.value = ''
}
const handleKeyPress = (e) => {
// 检测回车
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendAgent()
}
}
const handleSendAgent = () => {
if (!inputValue.value.trim()) return
emits('send', { text: inputValue.value.trim(), images: uploadedImages.value })
// 发送后清空输入框
if (editorRef.value) {
editorRef.value.innerHTML = ''
}
inputValue.value = ''
}
// 监听 inputValue 外部变化
watch(inputValue, () => {
nextTick(() => {
@@ -314,7 +324,6 @@
onMounted(() => {
autoResizeEditor()
})
const typeValue = ref<string>('')
const areaValue = ref<string>('')
@@ -598,7 +607,7 @@
.agent {
padding: 1.2rem;
box-shadow: none;
border-radius: 1.5rem;
border-radius: 1.5rem;
border: 0.1rem solid #0000001a;
.scroll-content {
padding: 0;
@@ -611,7 +620,7 @@
min-height: initial;
max-height: initial;
padding: 0;
height: 100%;
height: 100%;
}
}
.operate {

View File

@@ -1,5 +1,14 @@
<template>
<div class="home background-pink">
<div class="bg bg-1">
<div class="topright"></div>
<div class="bottomleft"></div>
</div>
<div class="bg bg-2">
<div class="bottom-1"></div>
<div class="bottom-2"></div>
<div class="bottom-3"></div>
</div>
<div class="home">
<left-nav />
<div class="right-main">
<top-nav />
@@ -38,4 +47,144 @@
}
}
}
.bg-1 {
z-index: -1;
animation: opacity-in 0.5s ease-in-out 1 both;
}
.bg-2 {
animation: z-index-10to-1 0.5s ease-in-out 1 both;
}
.bg {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
// background-color: rgba(248, 247, 245, 1);
> * {
position: absolute;
border-radius: 50%;
}
> .topright {
width: 174.7rem;
height: 42.1rem;
top: -15.7rem;
right: -70rem;
background: linear-gradient(
86.85deg,
rgba(244, 147, 116, 0.6) 13.62%,
rgba(241, 193, 145, 0.6) 25.57%,
rgba(255, 178, 91, 0.582) 41.03%,
rgba(242, 173, 126, 0.6) 59.37%,
rgba(255, 232, 200, 0.6) 75.27%
);
opacity: 0.2;
filter: blur(20.7656rem);
transform: rotate(178.95deg);
}
> .bottomleft {
// border-radius: 0;
// border: 1px solid #000;
width: 62.418rem;
height: 68.245rem;
left: -30rem;
bottom: 0;
background: linear-gradient(
161.16deg,
rgba(132, 230, 255, 0.2) 14.98%,
rgba(255, 223, 142, 0.2) 68.79%
);
filter: blur(8.263rem);
transform: rotate(-25.36deg);
}
> .bottom-1 {
animation: bottom-1 0.5s ease-in-out 1 both;
background: linear-gradient(
87.58deg,
rgba(241, 193, 145, 0.8) 23.02%,
rgba(255, 178, 91, 0.776) 35.36%,
rgba(244, 147, 116, 0.8) 56.32%,
rgba(242, 173, 126, 0.8) 67.34%,
rgba(255, 232, 200, 0.8) 82.74%
);
filter: blur(13.17rem);
transform: matrix(-1, 0.03, -0.05, -1, 0, 0);
}
> .bottom-2 {
animation: bottom-2 0.5s ease-in-out 1 both;
background: conic-gradient(
from 94.36deg at 71.77% 41.01%,
rgba(242, 171, 180, 0.2) 0deg,
rgba(255, 105, 117, 0.2) 100.75deg,
rgba(254, 59, 83, 0.2) 179.32deg,
rgba(255, 105, 117, 0.2) 252deg,
rgba(242, 171, 180, 0.2) 360deg
);
filter: blur(12.927rem);
transform: matrix(-0.05, 1, -1, -0.03, 0, 0);
}
> .bottom-3 {
animation: bottom-3 0.5s ease-in-out 1 both;
background: linear-gradient(
130.72deg,
rgba(242, 171, 180, 0.24) 29.52%,
rgba(234, 133, 200, 0.24) 39.73%,
rgba(238, 64, 173, 0.24) 55.81%,
rgba(234, 133, 158, 0.24) 69.59%,
rgba(242, 171, 180, 0.24) 82.61%
);
filter: blur(11.5411rem);
transform: matrix(-0.26, -0.97, 0.99, -0.15, 0, 0);
}
@keyframes bottom-1 {
0% {
width: 15rem;
height: 15rem;
left: 50%;
bottom: 50%;
transform: translate(0, 50%);
}
100% {
width: 138.014rem;
height: 29.323rem;
left: 32.123rem;
bottom: -21rem;
transform: translate(0, 0);
}
}
@keyframes bottom-2 {
0% {
width: 15rem;
height: 15rem;
left: 50%;
bottom: 50%;
transform: translate(0, 50%);
}
100% {
width: 42.215rem;
height: 98.009rem;
left: 150rem;
bottom: -65rem;
transform: translate(0, 0);
}
}
@keyframes bottom-3 {
0% {
width: 15rem;
height: 15rem;
left: 50%;
bottom: 50%;
transform: translate(0, 50%);
}
100% {
width: 51.936rem;
height: 97.139rem;
left: 40rem;
bottom: -65rem;
transform: translate(0, 0);
}
}
}
</style>