提交 b5529599 authored 作者: 朱政's avatar 朱政

feat:多智库报告流式显示思考过程功能以及图表ai解读流式显示思考过程功能及样式开发

上级 82ef459c
// 智库概览信息 // 智库概览信息
import request from "@/api/request.js"; import request, { getToken } from "@/api/request.js";
// 智库列表 // 智库列表
export function getThinkTankList() { export function getThinkTankList() {
...@@ -162,6 +162,7 @@ export function getThinkDynamicsReport(params) { ...@@ -162,6 +162,7 @@ export function getThinkDynamicsReport(params) {
// 智库领域观点分析(流式) // 智库领域观点分析(流式)
// [POST] 8.140.26.4:10029/report-domain-view-analysis // [POST] 8.140.26.4:10029/report-domain-view-analysis
// 每次请求体:{ domain, report_view_list }(一个 domain);多领域由前端按领域循环多次调用
export function postReportDomainViewAnalysis(data) { export function postReportDomainViewAnalysis(data) {
return request({ return request({
method: 'POST', method: 'POST',
...@@ -171,6 +172,86 @@ export function postReportDomainViewAnalysis(data) { ...@@ -171,6 +172,86 @@ export function postReportDomainViewAnalysis(data) {
}) })
} }
/**
* 智库领域观点分析(真正流式,逐 chunk 回调)
* @param {object} data
* @param {{ onReasoningChunk?: (chunk: string) => void, onMessage?: (msg: any) => void }} handlers
*/
export async function postReportDomainViewAnalysisStream(data, handlers = {}) {
const { onReasoningChunk, onMessage } = handlers
const token = getToken()
const response = await fetch('/intelligent-api/report-domain-view-analysis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { token } : {})
},
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error(`流式分析请求失败: ${response.status}`)
}
// 兜底:非流式返回时仍可读取文本继续后续解析
if (!response.body) {
return await response.text()
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let done = false
let pending = ''
let fullText = ''
while (!done) {
const result = await reader.read()
done = result.done
if (result.value) {
const chunkText = decoder.decode(result.value, { stream: !done })
fullText += chunkText
pending += chunkText
const lines = pending.split(/\r?\n/)
pending = lines.pop() ?? ''
for (const rawLine of lines) {
const line = String(rawLine || '').trim()
if (!line || !line.startsWith('data:')) continue
const jsonText = line.slice(5).trim()
if (!jsonText || jsonText === '[DONE]') continue
try {
const msg = JSON.parse(jsonText)
if (typeof onMessage === 'function') onMessage(msg)
if (msg?.type === 'reasoning' && msg?.chunk != null && typeof onReasoningChunk === 'function') {
const c = String(msg.chunk).trim()
if (c) onReasoningChunk(c)
}
} catch (e) {
// 忽略非 JSON 数据行
}
}
}
}
// 处理最后一行残留
const last = String(pending || '').trim()
if (last.startsWith('data:')) {
const jsonText = last.slice(5).trim()
if (jsonText && jsonText !== '[DONE]') {
try {
const msg = JSON.parse(jsonText)
if (typeof onMessage === 'function') onMessage(msg)
if (msg?.type === 'reasoning' && msg?.chunk != null && typeof onReasoningChunk === 'function') {
const c = String(msg.chunk).trim()
if (c) onReasoningChunk(c)
}
} catch (e) {
// ignore
}
}
}
return fullText
}
//提出建议领域分布 //提出建议领域分布
export function getThinkPolicyIndustry(params) { export function getThinkPolicyIndustry(params) {
......
...@@ -67,12 +67,21 @@ ...@@ -67,12 +67,21 @@
<div class="box5"> <div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true"> <AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main"> <div class="box5-main">
<div class="box5Chart"> <div class="box5Chart">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 --> <!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%" <WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%"
height="100%" /> height="100%" />
</div> </div>
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
</div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -167,8 +176,8 @@ ...@@ -167,8 +176,8 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="isOpinionExpanded(item, index)" class="desc" <div v-if="isOpinionExpanded(item, index)" class="desc" v-html="highlightOpinionText(item.contentZh)">
v-html="highlightOpinionText(item.contentZh)"></div> </div>
<!-- <div class="right"> --> <!-- <div class="right"> -->
<!-- <div class="tag" v-for="(val, idx) in item.hylyList" :key="idx"> <!-- <div class="tag" v-for="(val, idx) in item.hylyList" :key="idx">
{{ val }} {{ val }}
...@@ -217,6 +226,9 @@ import { useRouter } from "vue-router"; ...@@ -217,6 +226,9 @@ import { useRouter } from "vue-router";
import "echarts-wordcloud"; import "echarts-wordcloud";
import AiSummary from '@/components/base/Ai/AiSummary/index.vue' import AiSummary from '@/components/base/Ai/AiSummary/index.vue'
import { getPersonSummaryInfo } from "@/api/common/index"; import { getPersonSummaryInfo } from "@/api/common/index";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import TipTab from "@/views/thinkTank/TipTab/index.vue";
const router = useRouter(); const router = useRouter();
...@@ -240,6 +252,17 @@ const props = defineProps({ ...@@ -240,6 +252,17 @@ const props = defineProps({
default: () => ({}) default: () => ({})
} }
}); });
const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网";
const isShowAiContentBox5 = ref(false);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => {
isShowAiContentBox5.value = val;
if (val) {
fetchBox5ChartInterpretation();
}
};
const searchOpinions = ref(''); const searchOpinions = ref('');
...@@ -578,6 +601,67 @@ const handleGetBox3AnalysisContent = async textJson => { ...@@ -578,6 +601,67 @@ const handleGetBox3AnalysisContent = async textJson => {
const res = await getChartAnalysis(params); const res = await getChartAnalysis(params);
console.log("图表解析内容", res); console.log("图表解析内容", res);
}; };
const getInterpretationTextFromChartResponse = (res) => {
const list = res?.data;
const first = Array.isArray(list) ? list[0] : null;
return (
first?.["解读"] ||
first?.["interpretation"] ||
first?.["analysis"] ||
first?.["content"] ||
""
);
};
const appendAiInterpretationChunk = (targetRef, chunk, loadingText = "解读生成中…") => {
if (!chunk) {
return;
}
const current = String(targetRef.value || "");
const base = current === loadingText ? "" : current;
targetRef.value = base + String(chunk);
};
const fetchBox5ChartInterpretation = async () => {
const list = Array.isArray(box5Data.value) ? box5Data.value : [];
if (!list.length) {
aiContentBox5.value = "暂无图表数据";
return;
}
const hasValidContent =
aiContentBox5.value &&
aiContentBox5.value !== "解读生成中…" &&
aiContentBox5.value !== "解读加载失败" &&
aiContentBox5.value !== "暂无图表数据";
if (hasValidContent || isBox5InterpretLoading.value) {
return;
}
isBox5InterpretLoading.value = true;
aiContentBox5.value = "解读生成中…";
const chartPayload = {
type: "词云图",
name: "报告关键词云",
data: list.map((item) => ({
name: item.name,
value: item.value
}))
};
try {
const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) {
console.error("报告关键词云图表解读请求失败", error);
aiContentBox5.value = "解读加载失败";
} finally {
isBox5InterpretLoading.value = false;
}
};
onMounted(() => { onMounted(() => {
handleGetThinkTankReportAbstract(); handleGetThinkTankReportAbstract();
...@@ -817,6 +901,7 @@ onMounted(() => { ...@@ -817,6 +901,7 @@ onMounted(() => {
width: 480px; width: 480px;
height: 415px; height: 415px;
.box5-main { .box5-main {
width: 480px; width: 480px;
height: 361px; height: 361px;
...@@ -825,8 +910,10 @@ onMounted(() => { ...@@ -825,8 +910,10 @@ onMounted(() => {
padding-top: 26px; padding-top: 26px;
padding-bottom: 43px; padding-bottom: 43px;
display: flex; display: flex;
flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
position: relative;
.box5Chart { .box5Chart {
width: 418px; width: 418px;
...@@ -834,6 +921,33 @@ onMounted(() => { ...@@ -834,6 +921,33 @@ onMounted(() => {
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
} }
.box5-footer {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 18px;
right: 0;
}
.ai-content {
position: absolute;
bottom: 0;
right: 0;
min-width: 480px;
min-height: 156px;
}
.ai-wrap {
position: relative;
cursor: pointer;
}
} }
} }
......
...@@ -678,6 +678,16 @@ const getInterpretationTextFromChartResponse = (res) => { ...@@ -678,6 +678,16 @@ const getInterpretationTextFromChartResponse = (res) => {
); );
}; };
/** 参考科技法案数量变化趋势:按 chunk 增量拼接展示 */
const appendAiInterpretationChunk = (targetRef, chunk, loadingText = "解读生成中…") => {
if (!chunk) {
return;
}
const current = String(targetRef.value || "");
const base = current === loadingText ? "" : current;
targetRef.value = base + String(chunk);
};
/** 政策追踪-领域分布饼图解读(与概览页 box6 入参一致) */ /** 政策追踪-领域分布饼图解读(与概览页 box6 入参一致) */
const fetchPolicyPtBox1ChartInterpretation = async () => { const fetchPolicyPtBox1ChartInterpretation = async () => {
const list = Array.isArray(box1Data.value) ? box1Data.value : []; const list = Array.isArray(box1Data.value) ? box1Data.value : [];
...@@ -705,7 +715,14 @@ const fetchPolicyPtBox1ChartInterpretation = async () => { ...@@ -705,7 +715,14 @@ const fetchPolicyPtBox1ChartInterpretation = async () => {
})) }))
}; };
try { try {
const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) }); const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentPolicyPt1, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentPolicyPt1.value = text || aiContentPolicyPt1.value || "未返回有效解读内容"; aiContentPolicyPt1.value = text || aiContentPolicyPt1.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
...@@ -743,7 +760,14 @@ const fetchPolicyPtBox2ChartInterpretation = async () => { ...@@ -743,7 +760,14 @@ const fetchPolicyPtBox2ChartInterpretation = async () => {
})) }))
}; };
try { try {
const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) }); const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentPolicyPt2, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentPolicyPt2.value = text || aiContentPolicyPt2.value || "未返回有效解读内容"; aiContentPolicyPt2.value = text || aiContentPolicyPt2.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
...@@ -789,7 +813,14 @@ const fetchPolicyPtBox3ChartInterpretation = async () => { ...@@ -789,7 +813,14 @@ const fetchPolicyPtBox3ChartInterpretation = async () => {
}) })
}; };
try { try {
const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) }); const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentPolicyPt3, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentPolicyPt3.value = text || aiContentPolicyPt3.value || "未返回有效解读内容"; aiContentPolicyPt3.value = text || aiContentPolicyPt3.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
......
...@@ -1041,6 +1041,16 @@ const getInterpretationTextFromChartResponse = (res) => { ...@@ -1041,6 +1041,16 @@ const getInterpretationTextFromChartResponse = (res) => {
); );
}; };
/** 参考科技法案数量变化趋势:按 chunk 增量拼接展示 */
const appendAiInterpretationChunk = (targetRef, chunk, loadingText = "解读生成中…") => {
if (!chunk) {
return;
}
const current = String(targetRef.value || "");
const base = current === loadingText ? "" : current;
targetRef.value = base + String(chunk);
};
/** 请求 box5 折线图解读并更新 aiContentBox5(支持流式 SSE) */ /** 请求 box5 折线图解读并更新 aiContentBox5(支持流式 SSE) */
const fetchBox5ChartInterpretation = async () => { const fetchBox5ChartInterpretation = async () => {
const v = box5ChartDisplayData.value; const v = box5ChartDisplayData.value;
...@@ -1070,7 +1080,14 @@ const fetchBox5ChartInterpretation = async () => { ...@@ -1070,7 +1080,14 @@ const fetchBox5ChartInterpretation = async () => {
}) })
}; };
try { try {
const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) }); const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox5, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容"; aiContentBox5.value = text || aiContentBox5.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
...@@ -1201,7 +1218,14 @@ const fetchBox6ChartInterpretation = async () => { ...@@ -1201,7 +1218,14 @@ const fetchBox6ChartInterpretation = async () => {
})) }))
}; };
try { try {
const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) }); const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox6, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox6.value = text || aiContentBox6.value || "未返回有效解读内容"; aiContentBox6.value = text || aiContentBox6.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
...@@ -1419,7 +1443,14 @@ const fetchBox7ChartInterpretation = async () => { ...@@ -1419,7 +1443,14 @@ const fetchBox7ChartInterpretation = async () => {
})) }))
}; };
try { try {
const res = await getChartAnalysis({ text: JSON.stringify(chartPayload) }); const res = await getChartAnalysis(
{ text: JSON.stringify(chartPayload) },
{
onChunk: chunk => {
appendAiInterpretationChunk(aiContentBox7, chunk);
}
}
);
const text = getInterpretationTextFromChartResponse(res); const text = getInterpretationTextFromChartResponse(res);
aiContentBox7.value = text || aiContentBox7.value || "未返回有效解读内容"; aiContentBox7.value = text || aiContentBox7.value || "未返回有效解读内容";
} catch (error) { } catch (error) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论