Files
deerflow-code/situation-overview-web/src/components/InfoChatView/index.vue
T
2026-05-23 17:23:15 +08:00

549 lines
18 KiB
Vue

<template>
<!-- @click="getChatInfo()" -->
<div class="frame">
<div class="opentiong">
<div class="isShowBtn" @click="getChatInfo()">
<img src="@/assets/icon/刷新.svg" alt="" />
</div>
<div class="isShowBtn" @click="quanpingModel = !quanpingModel">
<img src="@/assets/icon/流式输出.svg" alt="" />
</div>
<div class="isShowBtn" @click="isShow = !isShow">
<img src="@/assets/map-icon/expand-logo.svg" alt="" />
</div>
</div>
<div class="huakuai" v-if="isShow" @mousedown.prevent="startSplitting" @mouseup.prevent="endSplitting">
</div>
<div class="InfoChatView" ref="chatList"
:style="isShow ? { width: currentWidth + 'px' } : { width: '0px', border: 'none' }">
<t-collapse :default-value="[1]" @change="handlePanelChange" default-expand-all>
<t-collapse-panel header="思考过程"> <t-collapse default-expand-all>
<template v-for="(item, index) in chatList">
<t-collapse-panel header="思考过程" v-if="item.type === 'show_text'">
<CodeData :markdown="item.text"></CodeData>
</t-collapse-panel>
<t-collapse-panel header="分析报告" v-if="item.type === 'summary'">
<CodeData :markdown="item.text"></CodeData>
</t-collapse-panel>
<template v-if="item.type === 'google_search' && item.data['q']">
<div class="search" v-if="index === chatList.length - 1">
<t-loading text="搜索中..." size="small"></t-loading>{{
item.data['q'] }}
</div>
</template>
<t-collapse-panel :header="chatList[index - 1].data['q'] || '思考过程'"
v-if="item.type === 'google_search' && item.data['result']">
<div class="resultList">
<div class="item" @click="openDetails(item)"
v-for="(item, i) in JSON.parse(item.data['result']).organic">
<div class="eventTitle text-ellipsis ellipsis1">
<span class="num"> {{ i + 1 }} </span> {{ item.title }}
</div>
<div class="eventContent text-ellipsis ellipsis3">
{{ item.content }}
</div>
</div>
</div>
</t-collapse-panel>
</template>
</t-collapse></t-collapse-panel>
</t-collapse>
<div class="summary">
<CodeData :markdown="summary"></CodeData>
</div>
<t-loading v-if="loading" :loading="loading" show-overlay></t-loading>
</div>
</div>
<t-dialog v-model:visible="detailModel" header="详情" :zIndex="2000" width="1000" :footer='false'>
<t-space direction=" vertical" style="width: 100%">
<div class="detailModel">
<div class="title">
{{ detailData.title }}
</div>
<!-- <a class="link" target="_blank"> 原文链接</a> -->
<t-link hover="color"
:href="'http://chlib.b.uat.4.cn/knowlibrary/#/knowledge/mid-detail?recUuid=' + detailData.recUuid"
target="_blank">
<template #suffix-icon>
<jump-icon />
</template>
跳转链接
</t-link>
<div class="content">
<CodeData :markdown="detailData.content" />
</div>
</div>
</t-space>
</t-dialog>
<t-dialog v-model:visible="quanpingModel" header="详情" width="1600" :footer='false'>
<t-space direction=" vertical" style="width: 100%">
<div style="display: flex;flex-direction: column;gap: 10px;">
<t-collapse :default-value="[1]" @change="handlePanelChange" default-expand-all>
<t-collapse-panel header="思考过程"> <t-collapse default-expand-all>
<template v-for="(item, index) in chatList">
<t-collapse-panel header="思考过程" v-if="item.type === 'show_text'">
<CodeData :markdown="item.text"></CodeData>
</t-collapse-panel>
<t-collapse-panel header="分析报告" v-if="item.type === 'summary'">
<CodeData :markdown="item.text"></CodeData>
</t-collapse-panel>
<template v-if="item.type === 'google_search' && item.data['q']">
<div class="search" v-if="index === chatList.length - 1">
<t-loading text="搜索中..." size="small"></t-loading>{{
item.data['q'] }}
</div>
</template>
<t-collapse-panel :header="chatList[index - 1].data['q'] || '思考过程'"
v-if="item.type === 'google_search' && item.data['result']">
<div class="resultList">
<div class="item" @click="openDetails(item)"
v-for="(item, i) in JSON.parse(item.data['result']).organic">
<div class="eventTitle text-ellipsis ellipsis1">
<span class="num"> {{ i + 1 }} </span> {{ item.title }}
</div>
<div class="eventContent text-ellipsis ellipsis3">
{{ item.content }}
</div>
</div>
</div>
</t-collapse-panel>
</template>
</t-collapse></t-collapse-panel>
</t-collapse>
<div class="summary">
<CodeData :markdown="summary"></CodeData>
</div>
<t-loading v-if="loading" :loading="loading" show-overlay></t-loading>
</div>
</t-space>
</t-dialog>
</template>
<script>
import { requestAxiosService } from "@/service/request-axios-service"; // 引入发送异步请求方法
import CodeData from '../markdown/CodeData.vue'
import { MessagePlugin } from 'tdesign-vue-next';
import { LinkIcon, JumpIcon } from 'tdesign-icons-vue-next';
export default {
data() {
return {
rawData: [],
chatList: [],
quanpingModel: false,
loading: false,
isShow: true,
service: requestAxiosService,
abortController: null,
MessagePlugin: MessagePlugin,
isResizing: false,
currentWidth: 320,
startX: 0, //开始的X坐标
startWidth: 0, //开始的宽度
detailModel: false,
detailData: {},
summary: '', //总结
};
},
props: ['prompt', 'generate', 'taskId', 'taskChatList', 'taskSummary'],
components: { CodeData, JumpIcon },
computed: {
},
methods: {
openDetails(d) {
console.log(d);
this.detailData = d
this.detailModel = true
},
startSplitting(e) {
this.isResizing = true
this.startX = e.pageX;
this.startWidth = this.currentWidth
console.log(document);
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.endResizing);
},
handleMouseMove(e) {
if (!this.isResizing) return;
const deltaX = this.startX - e.pageX;
const newWidth = this.startWidth + deltaX;
// 设置宽度限制
const minWidth = 320;
const maxWidth = 800;
this.currentWidth = Math.max(minWidth, Math.min(newWidth, maxWidth));
console.log(this.currentWidth);
},
endResizing() {
this.isResizing = false;
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.endResizing);
},
dataHandl() {
console.log(this.rawData);
console.log(this.chatList);
this.service
.post(
`${environment["b1ConsumerUrl"]}/cop/batchEnemyElement`,
{
"data": [JSON.stringify(this.chatList), this.summary, ...this.rawData],
"taskId": this.taskId,
}
)
.then((res) => {
console.log("热点事件", res);
this.$emit('upDataInfo')
});
},
async getChatInfo() {
if (this.loading) {
this.MessagePlugin.warning('正在生成中,请稍后!')
return
}
this.loading = true
this.chatList = []
this.summary = ''
this.abortController = new AbortController();
try {
const response = await fetch(`https://ch1.b.uat.4.cn/qq_b1_chat/generate_qq`, {
// const response = await fetch(`http://localhost:8095/generate_qq`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: this.prompt
}),
// signal: this.abortController.signal
});
// console.log(response);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }));
throw new Error(errorData.detail || `HTTP error ${response.status}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
console.log(done, value);
if (done) {
this.loading = false
this.dataHandl()
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
const lastLine = lines.pop();
buffer = lastLine || '';
console.log(lines);
for (let line of lines) {
if (!line.startsWith('data: ')) continue;
this.rawData.push(line.slice(6))
try {
// 解析SSE数据
// line = line.replace(/'/g, '"').trim();
const parsedData = JSON.parse(line.slice(6));
const { event, data } = parsedData;
// 处理不同类型的事件
switch (event) {
case 'task_created': {
// taskIdRef.current = data.task_id;
break;
}
case 'tool_call': {
this.handleToolCall(data);
break;
}
case 'message': {
// 处理消息事件
break;
}
case 'done': {
break;
}
case 'references': {
break;
}
default:
console.log(`未知事件类型: ${event}`, data);
}
} catch (error) {
console.error('JSON解析错误:', error);
console.log('原始数据:', line.slice(6));
}
}
}
} catch (err) {
this.loading = false
console.error('JSON解析错误:', err);
console.log('接口中断');
setTimeout(() => {
this.MessagePlugin.warning('数据异常中断,开始重试!')
this.getChatInfo()
}, 1000)
}
},
handleToolCall(d) {
console.log(d);
switch (d.tool_name) {
case 'show_text': {
const chatText = d.tool_input.text || '';
console.log(chatText);
if (chatText.includes('<think>')) {
// 添加新的思考过程
const thinkContent = chatText.replace('<think>', '').replace('</think>', '');
this.chatList.push({
text: thinkContent,
type: d.tool_name,
});
} else {
this.summary += chatText;
// 更新总结回答
}
break;
}
case 'google_search': {
this.chatList.push({
type: d.tool_name,
data: d.tool_input
})
}
}
this.$nextTick(() => {
this.scrollToBottom()
})
},
scrollToBottom() {
const container = this.$refs.chatList
// 平滑滚动
if (container && container.scrollTo) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
})
}
}
},
watch: {
generate(v) {
if (v) {
this.getChatInfo()
}
},
taskChatList(v) {
console.log(v);
if (v) {
this.chatList = v
}
},
taskSummary(d) {
if (d) {
this.summary = d
}
}
},
mounted() {
// this.getChatInfo()
},
};
</script>
<style lang="less" scoped>
.detailModel {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
.title {
color: #fff;
font-size: 32px;
line-height: 40px;
font-weight: 700;
text-align: center;
}
.link {
color: #fff;
text-align: center;
}
.content {
color: #fff;
font-size: 14px;
}
}
.huakuai {
width: 8px;
height: 50px;
background-color: #075176;
border: 1px solid #05244f;
border-radius: 3px;
position: absolute;
left: 0;
top: 50%;
z-index: 1001;
cursor: pointer;
}
:deep(.t-collapse-panel__body) {
background: rgba(3, 38, 80, 0.8);
}
:deep(.t-collapse) {
background-color: transparent;
}
.hidden {
height: 0 !important;
padding: 0 !important;
border: 0 !important;
}
.frame {
display: flex;
flex-direction: column;
gap: 10px;
top: 230px;
position: absolute;
align-items: end;
.opentiong {
display: flex;
align-items: center;
gap: 10px;
}
.isShowBtn {
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: #032650cc;
border: 1px solid;
border-color: #2b8ffe99;
border-radius: 4px 0 0 4px;
}
}
.InfoChatView {
z-index: 1000;
display: flex;
gap: 10px;
flex-direction: column;
// transition: width 0.3s ease;
overflow-y: auto;
width: 319px;
padding: 20px 0;
height: calc(100vh - 360px);
background: rgba(3, 38, 80, 0.8);
border: 1px solid;
border-color: rgba(43, 143, 254, 0.6);
border-radius: 4px;
backdrop-filter: blur(10px);
// 滚动条样式
&::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f8f8f8;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
}
.chatItem {
.search {
color: #fff;
}
.think {
padding: 10px;
border: 1px solid;
background: rgba(0, 98, 217, 0.1);
border: 1px solid;
border-color: rgba(85, 166, 254, 0.1);
border-radius: 4px;
}
}
.resultList {
display: flex;
flex-direction: column;
gap: 10px;
.item {
cursor: pointer;
.eventTitle {
color: #fff;
font-size: 20px;
font-weight: 600;
.num {
width: 16px;
height: 16px;
border-radius: 50%;
font-size: 14px;
display: inline-block;
text-align: center;
background-color: var(--bg-color);
}
}
.eventContent {
color: #fff;
}
}
}
.summary {
padding: 10px;
}
</style>