This commit is contained in:
lzp
2026-03-16 11:39:00 +08:00
11 changed files with 309 additions and 84 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 317.48 276">
<defs>
<style>
.cls-1 {
fill: #3d3d3d;
}
</style>
</defs>
<path class="cls-1" d="M163.55,24.01v45c1.72.15,3.22.97,4.68,1.82,25.29,14.72,55.93,30.25,79.63,46.38,3.63,2.47,7.09,3.95,6.74,9.26l.44,87.56,42.03,21.97,1.47-.48c-1.67-3.38-6.49-11.59-6.02-14.95.43-3.07,4.76-6.02,7.72-4.83,6.02,2.42,13.49,26.46,17.23,32.78-.48,3.25-2.61,5.32-5.68,6.25-4.74,1.44-23.8,4.07-28.91,4.18-3.4.07-6.4-1-7.16-4.66-2.05-9.87,12.33-6.96,17.82-9.76l-44.27-22.35-90.3,53.84-5.1-1.82-86.04-52.02-44.27,22.35c5.79,2.51,19.81.16,17.82,9.76-.76,3.66-3.75,4.73-7.16,4.66-5.09-.11-24.21-2.74-28.91-4.18C2.36,253.86,0,251.79,0,248.51c0-1.92,12.92-30.45,14.27-31.54,3.42-2.77,8.94-1.29,10.12,2.77,1.07,3.71-4.02,12.15-5.82,15.78l1.47.48,42.03-21.97.94-91.06,1.52-2.48,89.03-51.47V24.01c-4.23,2.5-6.74,10.94-11.74,11.85-4.41.8-7.9-2.86-7.11-7.11.29-1.59,16.86-24.35,18.85-26.23,3.6-3.39,6.45-3.32,9.99,0,1.99,1.87,18.56,24.64,18.85,26.23.79,4.25-2.7,7.91-7.11,7.11-5-.91-7.51-9.35-11.74-11.85ZM235.55,126l-1.66-2.82-74.9-43.16-77.44,45.49,76.58,45.51,77.43-45.01ZM152.55,258.01v-75.5l-78-46.5v75.5l74.45,45.55,3.55.95ZM242.55,136.01l-78,46.5v75.5l3.55-.95,74.45-45.55v-75.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

View File

@@ -166,9 +166,15 @@ defineExpose({open})
<div class="modelBox">
<div class="model" ref="threeDom">
</div>
<div class="icon" v-show="!load.state">
<SvgIcon name="threeLogo" size="40" size-unit="px" />
</div>
<div class="load" v-show="load.state">
<i class="fi fi-rr-cubes"></i>
<div class="text">Load...</div>
<div class="text">
<img src="@/assets/images/threeLoading.png" alt="">
{{ $t('threeModel.loading') }}
</div>
<div class="loadBox">
<div class="schedule" :style="{width:load.progress+'%'}"></div>
</div>
@@ -190,30 +196,49 @@ defineExpose({open})
border: 1px solid #D9D9D9;
overflow: hidden;
}
> .icon{
position: absolute;
bottom: 2.4rem;
right: 2.4rem;
}
> .load{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: rgba(0, 0, 0, .2);
background: #e6e6e6;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: #fff;
> .text{
font-weight: 500;
font-size: 1.8rem;
line-height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #747474;
> img{
margin-right: 1rem;
}
}
> i{
font-size: 3rem;
}
> .loadBox{
width: 15rem;
width: 26rem;
height: 1rem;
border-radius: 1rem;
border-radius: 3.3rem;
background: #fff;
overflow: hidden;
margin-top: 1.2rem;
> .schedule{
height: 100%;
background: greenyellow;
border-radius: 3.3rem;
background: #848484;
}
}
}

View File

@@ -80,7 +80,7 @@
text-align: center;
line-height: 18px;
background-color: #fff;
font-size: 6px;
font-size: 9px;
color: #000;
border: 1px solid #d9d9d9;
cursor: pointer;

View File

@@ -187,5 +187,9 @@ export default {
},
assistant: {
inputPlaceholder: 'Ask anything',
},
//3d面板
threeModel: {
loading: 'Loading',
}
}

View File

@@ -182,5 +182,9 @@ export default {
},
assistant: {
inputPlaceholder: '请输入'
},
//3d面板
threeModel: {
loading: '加载中',
}
}

View File

@@ -33,7 +33,10 @@
const agentStore = useAgentStore()
const projectStore = useProjectStore()
const emits = defineEmits(['update:sketchList'])
const reportsContent = ref('')
const isGeneratingReport = ref(false)
const emits = defineEmits(['update:sketchList', 'setTitle'])
const props = withDefaults(
defineProps<{
title: string
@@ -143,6 +146,7 @@
id: messageList.value.length + 1,
text: '',
isUser: false,
sessionId: 'projectStore.state.id',
loading: true,
thinking: false,
thinkingText: '',
@@ -206,11 +210,13 @@
// 流式响应处理
let contentBody = ''
let buffer = ''
const webAddressList = []
const reader = response.body?.getReader()
if (!reader) throw new Error('无法获取流读取器')
const decoder = new TextDecoder()
let previousEventName = '' // 记录上一个事件名称
let hasReportStarted = false // 标记 report 是否已经开始
try {
let flag = true
while (flag) {
@@ -232,16 +238,41 @@
for (let event of events) {
if (!event.trim()) continue
// debugger
// 过滤掉 id: 等字段,只取 data:
let isNodeIdEvent = false
if (event.includes('nodeId')) {
isNodeIdEvent = true
// continue
// 解析事件名称(从 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
}
console.log('event', event)
if (event.includes('error')) {
if (
previousEventName === 'report' &&
eventName !== 'report' &&
reportsContent.value
) {
isGeneratingReport.value = false
localStorage.setItem(
'reportsContent_' + projectStore.state.id,
reportsContent.value
)
}
previousEventName = eventName
// console.log('eventName:', eventName, 'event:', event)
// 根据事件名称精确判断,而不是用 includes
if (eventName === 'error') {
aiMessage.text = '出现错误,请重试'
aiMessage.streaming = false
aiMessage.loading = false
@@ -249,27 +280,29 @@
flag = false
break
}
// TODO: 暂时不处理webAddress
if (event.includes('todo')) {
if (eventName === 'todo') {
break
}
let hasSketch = false
if (event.includes('sketchIDAndUrl')) {
hasSketch = true
}
let isNodeIdEvent = eventName === 'nodeId'
let hasSketch = eventName === 'sketch'
const dataLines = event
.split(/\n/)
.filter((line) => line.startsWith('data:'))
.map((line) => line.replace(/^data:\s*/, '').trim())
.map((line) => line.replace(/^data:\s*/, ''))
.filter((content) => content.startsWith('{') || content.startsWith('['))
console.log('dataLInes', dataLines)
// console.log('dataLInes', dataLines)
if (isNodeIdEvent) {
params.versionID = dataLines[0]
projectStore.setProject({ nodeId: dataLines[0] })
}
if (eventName === 'webAddress') {
console.log('webAddress-----', eventName, dataLines)
}
if (event.includes('tool')) {
if (eventName === 'tool') {
MyEvent.emit('loading-sketch')
}
@@ -278,21 +311,38 @@
try {
const jsonData = JSON.parse(jsonText)
console.log('jsonData', jsonData)
// console.log('jsonData', jsonData)
if (jsonData.webAddress) {
console.log('webAddress-----', jsonData)
}
if (jsonData.title) {
emits('setTitle', jsonData.title)
}
if (hasSketch) {
sketchList.value.push({
[Object.keys(jsonData)[0]]: jsonData[Object.keys(jsonData)[0]]
})
}
if (
jsonData.content &&
jsonData.content.length > 0 &&
jsonData.type !== 'end'
) {
contentBody += jsonData.content
aiMessage.text = contentBody
aiMessage.loading = false
if (eventName === 'report') {
reportsContent.value += jsonData.report
} 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
}
}
}
if (jsonData.type === 'end') {
aiMessage.streaming = false
@@ -319,6 +369,9 @@
console.warn('⚠️ JSON 格式错误,跳过:', jsonText)
}
}
// 更新上一个事件名称
previousEventName = eventName
}
}
} catch (error) {
@@ -378,7 +431,7 @@
}
// 处理对话列表,将连续的 assistant 消息合并为一条
const processDialogue = (dialogue, startIndex, existingImgList) => {
const processDialogue = (dialogue, startIndex, existingImgList, sessionId) => {
if (!dialogue || dialogue.length === 0) return []
const result = []
@@ -393,26 +446,30 @@
...item,
text: item.content,
isUser: true,
id: result.length + 1
id: result.length + 1,
sessionId: sessionId
})
i++
} else if (item.role === 'assistant') {
// assistant 角色,拼接直到下一个 user
let combinedContent = item.content || ''
let combinedThinkingText = item.reasoning || ''
// 继续往后找连续的 assistant 消息
let j = i + 1
while (j < dialogue.length && dialogue[j].role === 'assistant') {
combinedContent += dialogue[j].content || ''
combinedThinkingText += dialogue[j].reasoning || ''
j++
}
result.push({
...item,
content: combinedContent,
thinkingText: combinedThinkingText,
text: combinedContent,
isUser: false,
id: result.length + 1
id: result.length + 1,
sessionId: sessionId
})
i = j
@@ -422,7 +479,8 @@
...item,
text: item.content,
isUser: item.role === 'user',
id: result.length + 1
id: result.length + 1,
sessionId: sessionId
})
i++
}
@@ -430,6 +488,44 @@
return result
}
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)
}
const setChatInfo = (info) => {
const initialData = agentStore.getInitialProjectData
@@ -454,37 +550,25 @@
messageList.value = []
return
}
const { ancestors, current } = data
let imgList = []
const ancestorsList = []
let ancestorsIdCounter = 1
if (ancestors) {
ancestors.forEach((item) => {
if (item.sketchIDAndUrl) {
imgList = imgList.concat(item.sketchIDAndUrl)
}
const list = processDialogue(item.dialogue, 0, imgList)
// 重新设置 id
list.forEach((el) => {
el.id = ancestorsIdCounter++
})
ancestorsList.push(...list)
})
}
const currentList = processDialogue(current?.dialogue, 0, imgList)
if (current.sketchIDAndUrl) {
imgList = imgList.concat(current.sketchIDAndUrl)
}
// 处理单个会话ancestor 或 current
currentList.forEach((el, index) => {
el.id = index + 1 + ancestorsList.length
const imgList = []
const ancestorsList = []
const idCounterRef = { value: 1 }
// 处理所有 ancestors
ancestors?.forEach((item) => {
processSession(item, imgList, ancestorsList, idCounterRef)
})
// 处理 current
processSession(current, imgList, ancestorsList, idCounterRef)
// 延迟设置新数据,确保 UI 有时间响应清空操作
nextTick(() => {
messageList.value = [...ancestorsList, ...currentList]
messageList.value = [...ancestorsList]
params.versionID = current?.id
sketchList.value = imgList
})

View File

@@ -16,7 +16,7 @@
class="img-item"
/>
</div>
<div class="message-context" v-show="content.thinking">
<div class="message-context" v-show="content.thinkingText?.length > 0">
<div class="thinking">
<div
class="thinking-header flex align-center"
@@ -37,15 +37,16 @@
:markdown="content.text"
:rehype-plugins="[rehypeRaw]"
>
<template v-slot:s-ReportCard="" {children:children,...attrs}>
<ReportCard
:report="{ title: attrs.title, content: attrs.content }"
@click="handleClickReport(content)"
/>
<template v-slot:s-card="{ children: children, ...attrs }">
<Card :title="attrs.title" @click.native="handleClickReport" />
</template>
</VueMarkdown>
</div>
<div class="operate flex" :class="{ 'is-user': content.isUser }">
<div
v-show="!content.thinking"
class="operate flex"
:class="{ 'is-user': content.isUser }"
>
<template v-if="content.isUser">
<SvgIcon name="copy" size="16" color="#000" @click.stop="handleCopyText" />
</template>
@@ -81,11 +82,12 @@
import gsap from 'gsap'
import userThumb from '@/assets/images/user-thumb.jpg'
import agentThumb from '@/assets/images/agent-thumb.png'
import ReportCard from './ReportCard.vue'
import UrlCard from './UrlCard.vue'
import Card from './ReportCard.vue'
import Url from './UrlCard.vue'
import { VueMarkdown } from '@crazydos/vue-markdown'
import type { CustomAttrs } from '@crazydos/vue-markdown'
import rehypeRaw from 'rehype-raw'
import MyEvent from '@/utils/myEvent'
const { t } = useI18n()
@@ -94,6 +96,14 @@
isLast: Boolean
}>()
watch(
() => props.content,
(newVal) => {
console.log('newVal-----', newVal)
},
{ immediate: true }
)
const emit = defineEmits(['regenerate'])
const imageList = computed(() => {
@@ -200,7 +210,8 @@
props.content.thinkingCollapsed = !props.content.thinkingCollapsed
}
const handleClickReport = (data) => {
const handleClickReport = () => {
MyEvent.emit('openReport', props.content.sessionId)
// 点击显示报告
}
const handleClickUrls = (data) => {

View File

@@ -57,14 +57,21 @@
</div>
<div v-else class="report-content">
<div class="downBtnBox">
<div class="downBtn">
<div class="downBtn" @click="handleDownloadMd">
<div class="icon">
<SvgIcon name="reportDown" size="16"></SvgIcon>
</div>
<span>{{ $t('agent.Download') }}</span>
</div>
</div>
<div class="content"></div>
<div class="content">
<VueMarkdown
:custom-attrs="customAttrs"
:markdown="markdownContent"
:rehype-plugins="[rehypeRaw]"
>
</VueMarkdown>
</div>
</div>
</div>
</div>
@@ -82,6 +89,10 @@
const projectStore = useProjectStore()
import MyEvent from '@/utils/myEvent'
import { useI18n } from 'vue-i18n'
import { VueMarkdown } from '@crazydos/vue-markdown'
import type { CustomAttrs } from '@crazydos/vue-markdown'
import rehypeRaw from 'rehype-raw'
const { t } = useI18n()
const emits = defineEmits(['deleteSketch'])
@@ -99,6 +110,36 @@
}
)
const customAttrs: CustomAttrs = {
img: {
style: 'max-width: 100%;display:block;'
},
a: (node, combinedAttrs) => {
if (typeof node.properties.href === 'string') {
return { target: '_blank', rel: 'noopener noreferrer' }
} else {
return {}
}
}
}
const sessionId = ref('')
const markdownContent = ref('')
const setSessionId = (id: string) => {
sessionId.value = id
}
watch(
() => sessionId.value,
(newVal) => {
if (newVal) {
markdownContent.value = localStorage.getItem(`reportsContent_${newVal}`)
console.log('markdownContent-----', markdownContent.value);
}
}
)
// 图片加载完成时触发
const handleImageLoad = (index: number) => {
loadedStatus[index] = true
@@ -147,6 +188,20 @@
})
}
const handleDownloadMd = () => {
if (!markdownContent.value) return
const blob = new Blob([markdownContent.value], { type: 'text/markdown' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `report-${Date.now()}.md`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
const showLoading = ref(false)
const handleLoadingSketch = () => {
showLoading.value = true
@@ -166,6 +221,10 @@
onUnmounted(() => {
MyEvent.remove('loading-sketch', handleLoadingSketch)
})
defineExpose({
setSessionId
})
</script>
<style lang="less" scoped>

View File

@@ -1,22 +1,23 @@
<template>
<div class="report-card">
<div class="report-card-header">
<span>{{ report.title }}</span>
<span>{{ title }}</span>
</div>
<div class="report-card-content">
<span>{{ report.content }}</span>
<span>{{ title }}</span>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
report: any
title: string
}>()
</script>
<style lang="less" scoped>
.report-card {
cursor: pointer;
width:100%;
margin: 2.4rem 0;
min-height: 11.2rem;
@@ -24,9 +25,9 @@
background-size: 100% 100%;
padding: 2.9rem;
overflow: hidden;
&:first-of-type{
margin-top: 0;
}
// &:first-of-type{
// margin-top: 0;
// }
&-header {
font-family: 'Medium';
font-size: 1.6rem;

View File

@@ -4,9 +4,19 @@
<div class="btn" @click="versionTreeData.drawer = true">Version Tree</div>
</div>
<div class="content-wrapper">
<Agent ref="agentRef" :title="agentTitle" @update:sketchList="updateSketchList" />
<Agent
ref="agentRef"
:title="agentTitle"
@update:sketchList="updateSketchList"
@setTitle="handleSetTitle"
/>
<div class="preview-wrapper">
<Preview :type="previewType" :sketchList="sketchList" @deleteSketch="handleDeleteSketch" />
<Preview
ref="previewRef"
:type="previewType"
:sketchList="sketchList"
@deleteSketch="handleDeleteSketch"
/>
</div>
</div>
<VersionTreeIndex
@@ -19,7 +29,7 @@
</template>
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue'
import { ref, watch, onMounted, computed, onUnmounted } from 'vue'
import Agent from './components/Agent.vue'
import Preview from './components/Preview.vue'
import VersionTreeIndex from './components/versionTree/index.vue'
@@ -27,10 +37,13 @@
import { getProjectInfo } from '@/api/agent'
import { clearNodeChat, getNodeAncestors } from '@/api/versitonTree'
import { useRoute } from 'vue-router'
import MyEvent from '@/utils/myEvent'
const route = useRoute()
const projectStore = useProjectStore()
const previewRef = ref(null)
const agentTitle = ref('Conversation')
const previewType = ref<'sketch' | 'report'>('sketch')
const VersionTreeIndexRef = ref()
@@ -50,6 +63,10 @@
})
}
const handleSetTitle = (title: string) => {
agentTitle.value = title
}
const versionTreeData = ref({
drawer: false
})
@@ -86,6 +103,11 @@
const proJectId = computed(() => route.params.id)
const handleOpenReport = (data) => {
previewRef.value.setSessionId(data)
previewType.value = 'report'
}
watch(
() => proJectId.value,
(newVal, oldVal) => {
@@ -97,11 +119,15 @@
)
onMounted(() => {
MyEvent.add('openReport', handleOpenReport)
projectStore.clearProject()
if (proJectId.value) {
handleGetProjectInfoAndHistory()
}
})
onUnmounted(() => {
MyEvent.remove('openReport', handleOpenReport)
})
</script>
<style lang="less" scoped>