feat: 报告初步显示
This commit is contained in:
@@ -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
|
||||
@@ -206,6 +209,7 @@
|
||||
// 流式响应处理
|
||||
let contentBody = ''
|
||||
let buffer = ''
|
||||
const webAddressList = []
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) throw new Error('无法获取流读取器')
|
||||
|
||||
@@ -230,18 +234,43 @@
|
||||
let events = buffer.split(/\n\n/)
|
||||
buffer = events.pop() // 保留不完整块
|
||||
|
||||
let previousEventName = '' // 记录上一个事件名称
|
||||
let hasReportStarted = false // 标记 report 是否已经开始
|
||||
|
||||
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', reportsContent.value)
|
||||
}
|
||||
|
||||
previousEventName = eventName
|
||||
|
||||
// console.log('eventName:', eventName, 'event:', event)
|
||||
|
||||
// 根据事件名称精确判断,而不是用 includes
|
||||
if (eventName === 'error') {
|
||||
aiMessage.text = '出现错误,请重试'
|
||||
aiMessage.streaming = false
|
||||
aiMessage.loading = false
|
||||
@@ -249,27 +278,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 +309,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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
if (jsonData.type === 'end') {
|
||||
aiMessage.streaming = false
|
||||
@@ -319,6 +367,9 @@
|
||||
console.warn('⚠️ JSON 格式错误,跳过:', jsonText)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新上一个事件名称
|
||||
previousEventName = eventName
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -378,7 +429,7 @@
|
||||
}
|
||||
|
||||
// 处理对话列表,将连续的 assistant 消息合并为一条
|
||||
const processDialogue = (dialogue, startIndex, existingImgList) => {
|
||||
const processDialogue = (dialogue, startIndex, existingImgList, sessionId) => {
|
||||
if (!dialogue || dialogue.length === 0) return []
|
||||
|
||||
const result = []
|
||||
@@ -393,26 +444,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 +477,8 @@
|
||||
...item,
|
||||
text: item.content,
|
||||
isUser: item.role === 'user',
|
||||
id: result.length + 1
|
||||
id: result.length + 1,
|
||||
sessionId: sessionId
|
||||
})
|
||||
i++
|
||||
}
|
||||
@@ -430,6 +486,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 +548,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
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user