提交 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) {
......
...@@ -160,17 +160,48 @@ ...@@ -160,17 +160,48 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="box2"> <div class="box2">
<AnalysisBox title="共识观点列表" :showAllBtn="true" v-if="isBox2"> <AnalysisBox title="共识观点列表" :showAllBtn="true" v-if="isBox2 || isAnalysisLoading">
<div class="box2-main"> <div class="box2-main">
<div class="empty-image"> <div class="empty-image" v-if="isBox2 && !isAnalysisLoading">
<img src="../assets/images/empty-image.png" alt="" /> <img src="../assets/images/empty-image.png" alt="" />
</div> </div>
<div v-if="isAnalysisLoading && !isBeingAnalysisExpanded" class="being-analysis-box">
<div class="being-analysis-box-image">
<img src="../assets/images/being-analysis-circle.png" alt="" />
</div>
<div class="being-analysis-box-text">{{ "正在思考" }}</div>
<div class="being-analysis-box-change" @click="toggleBeingAnalysisBox">
<img src="../assets/images/blue-down.png" alt="" v-if="!isBeingAnalysisExpanded" />
<img src="../assets/images/blue-up.png" alt="" v-else />
</div>
</div>
<div v-if="isAnalysisLoading && isBeingAnalysisExpanded" class="being-analysis-detail-box">
<div class="being-analysis-box-header">
<div class="being-analysis-box-image">
<img src="../assets/images/being-analysis-circle.png" alt="" />
</div>
<div class="being-analysis-box-text">{{ "正在思考" }}</div>
<div class="being-analysis-box-change" @click="toggleBeingAnalysisBox">
<img src="../assets/images/blue-down.png" alt="" v-if="!isBeingAnalysisExpanded" />
<img src="../assets/images/blue-up.png" alt="" v-else />
</div>
</div>
<div class="being-analysis-box-content" ref="beingAnalysisContentRef">
<div class="being-analysis-content-line" v-for="(line, idx) in beingAnalysisChunks" :key="idx">
{{ line }}
</div> </div>
</AnalysisBox>
<AnalysisResultBox title="核心观点分析" :showAllBtn="false" v-if="!isBox2" @tab-change="handleOpinionTabChange">
<div v-if="isAnalysisLoading" class="analysis-loading">
正在加载
</div> </div>
</div>
<div v-if="isAnalysisLoading" class="being-analysis-image">
<img src="../assets/images/being-analysis.png" alt="" />
</div>
</div>
</AnalysisBox>
<AnalysisResultBox title="核心观点分析" :showAllBtn="false" v-if="!isBox2 && !isAnalysisLoading"
@tab-change="handleOpinionTabChange">
<div class="box2-main-Consensus" v-if="!isAnalysisLoading && activeOpinionTab === 'consensus'"> <div class="box2-main-Consensus" v-if="!isAnalysisLoading && activeOpinionTab === 'consensus'">
<!-- <div class="box2-main-Consensus-header"> <!-- <div class="box2-main-Consensus-header">
<div class="tech-development-view">{{ `共识观点 ${consensusList.length}条` }}</div> <div class="tech-development-view">{{ `共识观点 ${consensusList.length}条` }}</div>
...@@ -189,34 +220,33 @@ ...@@ -189,34 +220,33 @@
<div class="Consensus-item-title">{{ item.consensusContent }}</div> <div class="Consensus-item-title">{{ item.consensusContent }}</div>
<div class="Consensus-item-right">{{ `${item.reportCount}篇报告提及` }}</div> <div class="Consensus-item-right">{{ `${item.reportCount}篇报告提及` }}</div>
<div class="Consensus-item-image" @click="toggleConsensusItem(item.id)"> <div class="Consensus-item-image" @click="toggleConsensusItem(item.id)">
<img src="../assets/images/down.png" alt="" /> <img src="../assets/images/down.png" alt="" v-if="!openConsensusIds.has(item.id)" />
<!-- <img src="../assets/images/up.png" alt="" v-else /> --> <img src="../assets/images/up.png" alt="" v-else />
</div> </div>
</div> </div>
<!-- <div class="Consensus-expand" v-if="openConsensusIds.has(item.id)"> <div class="Consensus-expand" v-if="openConsensusIds.has(item.id)">
<div class="Consensus-expand-row">
<div class="Consensus-expand-title"> <div v-for="(sv, svIdx) in item.sourceViewDetails" :key="`${sv.report_id}-${sv.view_id}-${svIdx}`"
{{ "来源观点列表" }} class="source-view-detail">
</div> <div class="source-view-detail-title">
<div class="Consensus-expand-subtitle"> <span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx) }}</span>
<div class="Consensus-expand-image"> <span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img src="../assets/images/rand-image.png" alt="" /> <img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
</span>
</div> </div>
{{ `${item.reportCount}条来源观点` }} <div class="source-view-detail-content">{{ getSourceViewDisplayContent(sv) }}</div>
</div> </div>
<div class="Consensus-expand-badge is-tech-development">{{ "共识观点" }}</div>
</div> </div>
</template>
<div class="Consensus-expand-content"> <div class="empty-image" v-if="consensusList.length === 0">
{{ item.sourceViewText }} <img src="../assets/images/empty-analysis.png" alt="" />
</div> </div>
<div class="Consensus-expand-badge is-tech-development"> <div class="empty-text" v-if="consensusList.length === 0">
{{ "共识观点" }} {{ "未识别到共识观点..." }}
</div> </div>
</div> -->
</template>
</div> </div>
</div> </div>
<div class="box2-main-Differences" v-else-if="!isAnalysisLoading"> <div class="box2-main-Differences" v-else-if="!isAnalysisLoading">
...@@ -240,16 +270,22 @@ ...@@ -240,16 +270,22 @@
<div class="Differences-expand-content"> <div class="Differences-expand-content">
<div class="Differences-expand-top-content"> <div class="Differences-expand-top-content">
<div class="Differences-expand-top-content-left"> <div class="Differences-expand-top-content-left">
<div class="content-left-title">{{ "来源观点列表" }}</div>
<div class="content-left-img-name">
<div class="content-left-img"> <div class="differences-source-views">
<img src="../assets/images/rand-image.png" alt="" /> <div v-for="(sv, svIdx) in item.sourceViewDetails"
:key="`${sv.report_id}-${sv.view_id}-${svIdx}`" class="source-view-detail">
<div class="source-view-detail-title">
<span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx)
}}</span>
<span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
</span>
</div> </div>
<div class="content-left-name"> <div class="source-view-detail-content">{{ getSourceViewDisplayContent(sv) }}</div>
{{ `${item.reportCount}条来源观点` }}
</div> </div>
</div> </div>
<div class="content-left-text">{{ item.sourceViewText }}</div>
</div> </div>
</div> </div>
...@@ -261,7 +297,12 @@ ...@@ -261,7 +297,12 @@
</div> </div>
</div> </div>
</template> </template>
<div class="empty-image" v-if="differenceList.length === 0">
<img src="../assets/images/empty-analysis.png" alt="" />
</div>
<div class="empty-text" v-if="differenceList.length === 0">
{{ "未识别到分歧观点..." }}
</div>
</div> </div>
</div> </div>
</AnalysisResultBox> </AnalysisResultBox>
...@@ -271,15 +312,40 @@ ...@@ -271,15 +312,40 @@
</template> </template>
<script setup> <script setup>
import router from '@/router'; import router from '@/router';
import { onMounted, ref, computed, reactive, nextTick } from "vue"; import { onMounted, ref, computed, reactive, nextTick, watch } from "vue";
import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue" import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue"
import AnalysisResultBox from "./boxBackground/analysisBox.vue" import AnalysisResultBox from "./boxBackground/analysisBox.vue"
import { getThinkTankReport, getHylyList, getThinkTankReportViewpoint, postReportDomainViewAnalysis } from "@/api/thinkTank/overview"; import {
getThinkTankReport,
getHylyList,
getThinkTankReportViewpoint,
postReportDomainViewAnalysisStream
} from "@/api/thinkTank/overview";
const sort = ref(""); const sort = ref("");
const searchPolicy = ref(""); const searchPolicy = ref("");
const isBox2 = ref(true) const isBox2 = ref(true)
const isAnalysisLoading = ref(false) const isAnalysisLoading = ref(false)
const isBeingAnalysisExpanded = ref(false)
const beingAnalysisChunks = ref([])
const beingAnalysisContentRef = ref(null)
const activeOpinionTab = ref('consensus') const activeOpinionTab = ref('consensus')
const toggleBeingAnalysisBox = () => {
isBeingAnalysisExpanded.value = !isBeingAnalysisExpanded.value
}
const scrollBeingAnalysisToBottom = async () => {
await nextTick()
const el = beingAnalysisContentRef.value
if (!el) return
el.scrollTop = el.scrollHeight
}
watch(() => beingAnalysisChunks.value.length, async () => {
if (!isBeingAnalysisExpanded.value) return
await scrollBeingAnalysisToBottom()
})
watch(isBeingAnalysisExpanded, async (expanded) => {
if (!expanded) return
await scrollBeingAnalysisToBottom()
})
const handleOpinionTabChange = type => { const handleOpinionTabChange = type => {
activeOpinionTab.value = type activeOpinionTab.value = type
} }
...@@ -358,6 +424,33 @@ const domainName = computed(() => { ...@@ -358,6 +424,33 @@ const domainName = computed(() => {
return hit?.name || '全部领域' return hit?.name || '全部领域'
}) })
const domainViewAnalysisRes = ref(null) const domainViewAnalysisRes = ref(null)
/** 观点详情:key 为 `${reportId}::${viewId}`,与流式结果里 source_views 的 view_id 对应观点接口返回的 id */
const viewpointDetailByKey = ref({})
const getViewpointDetailForSource = (reportId, viewId) => {
const key = `${String(reportId)}::${String(viewId)}`
return viewpointDetailByKey.value[key] || {
titleZh: "",
contentZh: "",
title: "",
content: "",
thinktankName: "",
thinktankLogoUrl: ""
}
}
/** 展开区标题:优先中文标题,否则英文;无标题则返回空串(由上游过滤) */
const getSourceViewDisplayTitle = (sv, idx) => {
const zh = String(sv.titleZh ?? "").trim()
if (zh) return zh
const en = String(sv.title ?? "").trim()
if (en) return en
return ""
}
/** 展开区正文:优先中文,否则英文 */
const getSourceViewDisplayContent = (sv) => {
const zh = String(sv.contentZh ?? "").trim()
if (zh) return zh
return String(sv.content ?? "").trim()
}
const tryParseAnswerFromStreamText = (text) => { const tryParseAnswerFromStreamText = (text) => {
const lines = String(text || "") const lines = String(text || "")
.split(/\r?\n/) .split(/\r?\n/)
...@@ -398,12 +491,31 @@ const consensusList = computed(() => { ...@@ -398,12 +491,31 @@ const consensusList = computed(() => {
const list = Array.isArray(answer?.consensus) ? answer.consensus : [] const list = Array.isArray(answer?.consensus) ? answer.consensus : []
return list.map((item, index) => { return list.map((item, index) => {
const sourceViews = Array.isArray(item?.source_views) ? item.source_views : [] const sourceViews = Array.isArray(item?.source_views) ? item.source_views : []
const sourceViewDetails = sourceViews
.map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id)
return {
report_id: v.report_id,
view_id: v.view_id,
titleZh: detail.titleZh,
contentZh: detail.contentZh,
title: detail.title,
content: detail.content,
thinktankName: detail.thinktankName,
thinktankLogoUrl: detail.thinktankLogoUrl
}
})
.filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0)
return Boolean(title)
})
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";") const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return { return {
id: `consensus-${index + 1}`, id: `consensus-${index + 1}`,
consensusContent: item?.consensus_content || "", consensusContent: item?.consensus_content || "",
reportCount: sourceViews.length, reportCount: sourceViewDetails.length,
sourceViewText sourceViewText,
sourceViewDetails
} }
}) })
}) })
...@@ -412,12 +524,31 @@ const differenceList = computed(() => { ...@@ -412,12 +524,31 @@ const differenceList = computed(() => {
const list = Array.isArray(answer?.differences) ? answer.differences : [] const list = Array.isArray(answer?.differences) ? answer.differences : []
return list.map((item, index) => { return list.map((item, index) => {
const sourceViews = Array.isArray(item?.source_views) ? item.source_views : [] const sourceViews = Array.isArray(item?.source_views) ? item.source_views : []
const sourceViewDetails = sourceViews
.map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id)
return {
report_id: v.report_id,
view_id: v.view_id,
titleZh: detail.titleZh,
contentZh: detail.contentZh,
title: detail.title,
content: detail.content,
thinktankName: detail.thinktankName,
thinktankLogoUrl: detail.thinktankLogoUrl
}
})
.filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0)
return Boolean(title)
})
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";") const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return { return {
id: `difference-${index + 1}`, id: `difference-${index + 1}`,
disagreementContent: item?.disagreement_content || "", disagreementContent: item?.disagreement_content || "",
reportCount: sourceViews.length, reportCount: sourceViewDetails.length,
sourceViewText sourceViewText,
sourceViewDetails
} }
}) })
}) })
...@@ -456,6 +587,8 @@ const handleAnalysis = async () => { ...@@ -456,6 +587,8 @@ const handleAnalysis = async () => {
if (!canProceed.value) return if (!canProceed.value) return
isBox2.value = false isBox2.value = false
isAnalysisLoading.value = true isAnalysisLoading.value = true
isBeingAnalysisExpanded.value = false
beingAnalysisChunks.value = []
domainViewAnalysisRes.value = null domainViewAnalysisRes.value = null
// 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位 // 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位
activeOpinionTab.value = 'consensus' activeOpinionTab.value = 'consensus'
...@@ -464,6 +597,10 @@ const handleAnalysis = async () => { ...@@ -464,6 +597,10 @@ const handleAnalysis = async () => {
const handleBack = () => { const handleBack = () => {
isChoseItem.value = true isChoseItem.value = true
isBox2.value = true isBox2.value = true
// 返回选择时终止“分析中”展示,恢复空态图
isAnalysisLoading.value = false
isBeingAnalysisExpanded.value = false
beingAnalysisChunks.value = []
// 返回选择时也重置,确保下次进入分析展示一致 // 返回选择时也重置,确保下次进入分析展示一致
activeOpinionTab.value = 'consensus' activeOpinionTab.value = 'consensus'
} }
...@@ -524,10 +661,41 @@ const normalizeViewList = raw => { ...@@ -524,10 +661,41 @@ const normalizeViewList = raw => {
.filter(v => v.view_id != null && v.view_text !== "") .filter(v => v.view_id != null && v.view_text !== "")
} }
/** 从列表接口返回的报告项上取科技领域名称;没有再回退为左侧已选领域名,仍没有则用空字符串(不用「全部领域」) */
const getDomainsForReport = (report) => {
const raw = report?.domains
if (Array.isArray(raw) && raw.length) {
const names = raw.map((x) => String(x ?? "").trim()).filter(Boolean)
if (names.length) return [...new Set(names)]
}
if (selectedAreaList.value) {
const hit = Array.isArray(areaList.value)
? areaList.value.find((i) => String(i.id) === String(selectedAreaList.value))
: null
const name = String(hit?.name ?? "").trim()
if (name) return [name]
}
return [""]
}
const mergeAnalysisAnswerParts = (parts) => {
const consensus = []
const differences = []
for (const p of parts) {
const data = getAnalysisAnswerData(p)
if (!data) continue
if (Array.isArray(data.consensus)) consensus.push(...data.consensus)
if (Array.isArray(data.differences)) differences.push(...data.differences)
}
return { consensus, differences }
}
const handlePostReportDomainViewAnalysis = async () => { const handlePostReportDomainViewAnalysis = async () => {
try { try {
viewpointDetailByKey.value = {}
beingAnalysisChunks.value = []
const selectedReports = selectedReportList.value || [] const selectedReports = selectedReportList.value || []
const reportViewListRaw = await Promise.all( const reportEntriesRaw = await Promise.all(
selectedReports.map(async (report) => { selectedReports.map(async (report) => {
const reportId = report?.id const reportId = report?.id
if (!reportId) return null if (!reportId) return null
...@@ -535,16 +703,49 @@ const handlePostReportDomainViewAnalysis = async () => { ...@@ -535,16 +703,49 @@ const handlePostReportDomainViewAnalysis = async () => {
const viewpointRes = await getThinkTankReportViewpoint({ const viewpointRes = await getThinkTankReportViewpoint({
reportId, reportId,
currentPage: 0, currentPage: 0,
pageSize: 100, pageSize: 10000,
keyword: "", keyword: "",
orgIds: "" orgIds: ""
}) })
console.log("分析时用到的观点", viewpointRes)
if (viewpointRes?.code !== 200 || !viewpointRes?.data) return null if (viewpointRes?.code !== 200 || !viewpointRes?.data) return null
const views = normalizeViewList(viewpointRes.data.content ?? viewpointRes.data) const items = Array.isArray(viewpointRes.data.content)
? viewpointRes.data.content
: Array.isArray(viewpointRes.data)
? viewpointRes.data
: []
const detailMap = {}
for (const row of items) {
const vid = row?.id ?? row?.view_id ?? row?.viewId
if (vid == null || vid === "") continue
detailMap[`${reportId}::${String(vid)}`] = {
titleZh: String(row.titleZh ?? row.title_zh ?? "").trim(),
contentZh: String(row.contentZh ?? row.content_zh ?? "").trim(),
title: String(row.title ?? "").trim(),
content: String(row.content ?? "").trim(),
thinktankName: String(
row.thinktankName ??
row.thinktank_name ??
row.thinkTankName ??
row.orgName ??
""
).trim(),
thinktankLogoUrl: String(
row.thinktankLogoUrl ??
row.thinktank_logo_url ??
row.thinkTankLogoUrl ??
row.orgLogoUrl ??
""
).trim()
}
}
const views = normalizeViewList(items)
if (!views.length) return null if (!views.length) return null
return { return {
report,
report_id: reportId, report_id: reportId,
view_list: views view_list: views,
detailMap
} }
} catch (error) { } catch (error) {
console.error(`获取报告核心论点失败: ${reportId}`, error) console.error(`获取报告核心论点失败: ${reportId}`, error)
...@@ -553,20 +754,62 @@ const handlePostReportDomainViewAnalysis = async () => { ...@@ -553,20 +754,62 @@ const handlePostReportDomainViewAnalysis = async () => {
}) })
) )
const reportViewList = reportViewListRaw.filter(Boolean) const mergedDetails = {}
if (!reportViewList.length) { for (const e of reportEntriesRaw) {
if (e?.detailMap) Object.assign(mergedDetails, e.detailMap)
}
viewpointDetailByKey.value = mergedDetails
const reportEntries = reportEntriesRaw.filter(Boolean)
if (!reportEntries.length) {
domainViewAnalysisRes.value = null domainViewAnalysisRes.value = null
return return
} }
const payload = { // 按报告 domains 拆成多组 { domain, report_view_list };每个领域单独调一次接口(无最外层 domain_report_view_list
domain: domainName.value || "全部领域", const domainToReports = new Map()
report_view_list: reportViewList for (const entry of reportEntries) {
const domainLabels = getDomainsForReport(entry.report)
for (const domainLabel of domainLabels) {
if (!domainToReports.has(domainLabel)) {
domainToReports.set(domainLabel, [])
}
const list = domainToReports.get(domainLabel)
if (!list.some((x) => x.report_id === entry.report_id)) {
list.push({
report_id: entry.report_id,
view_list: entry.view_list
})
}
}
} }
const res = await postReportDomainViewAnalysis(payload) const payloads = [...domainToReports.entries()]
console.log('智库领域观点分析接口返回', res) .filter(([, report_view_list]) => report_view_list.length > 0)
domainViewAnalysisRes.value = res .map(([domain, report_view_list]) => ({ domain, report_view_list }))
if (!payloads.length) {
domainViewAnalysisRes.value = null
return
}
const responses = []
for (const payload of payloads) {
console.log("智库领域观点分析入参", payload)
const res = await postReportDomainViewAnalysisStream(payload, {
onReasoningChunk: async (chunk) => {
beingAnalysisChunks.value = [...beingAnalysisChunks.value, chunk]
await scrollBeingAnalysisToBottom()
}
})
responses.push(res)
}
const merged = mergeAnalysisAnswerParts(responses)
domainViewAnalysisRes.value = {
type: "answer",
data: merged
}
console.log("智库领域观点分析合并结果", merged)
} catch (e) { } catch (e) {
console.error('智库领域观点分析接口调用失败', e) console.error('智库领域观点分析接口调用失败', e)
domainViewAnalysisRes.value = null domainViewAnalysisRes.value = null
...@@ -736,16 +979,150 @@ onMounted(async () => { ...@@ -736,16 +979,150 @@ onMounted(async () => {
cursor: pointer; cursor: pointer;
} }
.analysis-loading { .being-analysis-box {
width: 1063px;
height: 40px;
background-color: rgb(246, 250, 255);
border-radius: 50px;
border: 1px solid rgb(231, 243, 255);
display: flex;
.being-analysis-box-image {
width: 16px;
height: 16px;
margin-top: 12px;
margin-left: 12px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.being-analysis-box-text {
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
height: 22px;
margin-left: 12px;
margin-top: 9px;
}
.being-analysis-box-change {
width: 14px;
height: 24px;
margin-top: 8px;
margin-left: auto;
margin-right: 12px;
cursor: pointer;
img {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 320px; display: block;
}
}
}
.being-analysis-detail-box {
width: 1063px;
height: 160px;
background-color: rgb(246, 250, 255);
border-radius: 10px;
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: center;
color: rgba(95, 101, 108, 1); .being-analysis-box-header {
font-size: 16px; width: 1063px;
height: 40px;
background-color: rgb(246, 250, 255);
display: flex;
.being-analysis-box-image {
width: 16px;
height: 16px;
margin-top: 12px;
margin-left: 12px;
img {
width: 100%;
height: 100%;
display: block;
}
}
.being-analysis-box-text {
font-family: "Source Han Sans CN";
font-weight: 700; font-weight: 700;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(5, 95, 194);
height: 22px;
margin-left: 12px;
margin-top: 9px;
}
.being-analysis-box-change {
width: 14px;
height: 24px;
margin-top: 8px;
margin-left: auto;
margin-right: 12px;
cursor: pointer;
img {
width: 100%;
height: 100%;
display: block;
}
}
}
.being-analysis-box-content {
width: 983px;
height: 104px;
margin-left: 40px;
overflow-y: auto;
overflow-x: hidden;
font-family: "Source Han Sans CN", sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 22px;
letter-spacing: 0;
text-align: left;
color: rgb(59, 65, 75);
}
}
.being-analysis-content-line {
line-height: 22px;
white-space: nowrap;
}
.being-analysis-image {
width: 100%;
height: 100%;
min-height: 320px;
display: flex;
top: 276px;
left: 408px;
position: absolute;
img {
width: 289px;
height: 242px;
display: block;
}
} }
.box { .box {
...@@ -1320,12 +1697,14 @@ onMounted(async () => { ...@@ -1320,12 +1697,14 @@ onMounted(async () => {
height: 866px; height: 866px;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
position: relative;
.empty-image { .empty-image {
width: 289px; width: 289px;
height: 215px; height: 242px;
margin-top: 276px; margin-top: 276px;
justify-content: center; justify-content: center;
display: flex; display: flex;
...@@ -1343,7 +1722,7 @@ onMounted(async () => { ...@@ -1343,7 +1722,7 @@ onMounted(async () => {
width: 100%; width: 100%;
display: flex; display: flex;
padding-top: 4px; padding-top: 13px;
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
padding-bottom: 18px; padding-bottom: 18px;
...@@ -1431,15 +1810,53 @@ onMounted(async () => { ...@@ -1431,15 +1810,53 @@ onMounted(async () => {
.box2-main-Consensus-content { .box2-main-Consensus-content {
width: 1056px; width: 1056px;
overflow-y: auto; overflow-y: auto;
margin-top: 16px; overflow-x: hidden;
height: 794px;
height: 100%;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: hidden; align-items: stretch;
position: relative;
.empty-text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: center;
display: flex;
width: 289px;
height: 30px;
position: absolute;
justify-content: center;
left: 384px;
bottom: 330px;
color: rgb(132, 136, 142);
}
/* 展开区向下撑开,子项不被纵向压缩;超出高度由本容器滚动 */
.empty-image {
height: 200px;
width: 200px;
position: absolute;
left: 428px;
bottom: 372px;
img {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
/* 防止图片变形 */
}
}
.Consensus-item { .Consensus-item {
width: 1056px; width: 1056px;
min-height: 62px; min-height: 62px;
flex-shrink: 0;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
...@@ -1525,12 +1942,13 @@ onMounted(async () => { ...@@ -1525,12 +1942,13 @@ onMounted(async () => {
.Consensus-expand { .Consensus-expand {
width: 1056px; width: 1056px;
flex-shrink: 0;
box-sizing: border-box; box-sizing: border-box;
border-top: none; border-top: none;
padding: 16px 50px 24px 58px; padding: 0px 50px 24px 58px;
} }
.Consensus-expand:last-child { .Consensus-expand:last-child {
...@@ -1620,6 +2038,82 @@ onMounted(async () => { ...@@ -1620,6 +2038,82 @@ onMounted(async () => {
border-bottom: 1px solid rgb(234, 236, 238); border-bottom: 1px solid rgb(234, 236, 238);
} }
.source-view-detail {
margin-top: 16px;
padding-bottom: 16px;
border-bottom: 1px solid rgb(234, 236, 238);
&:last-child {
border-bottom: none;
}
}
.source-view-detail-title {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
font-family: "Source Han Sans CN";
font-weight: 500;
font-size: 15px;
line-height: 24px;
color: rgb(59, 65, 75);
margin-bottom: 6px;
}
.source-view-detail-title-text {
justify-content: space-between;
gap: 16px;
font-family: "Source Han Sans CN";
font-weight: 700;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
}
.source-view-detail-org {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 4px;
color: rgb(95, 101, 108);
font-size: 14px;
line-height: 22px;
margin-top: 4px;
img {
width: 16px;
height: 16px;
display: block;
object-fit: cover;
margin-top: 3px;
}
.source-view-detail-org-text {
font-family: "Source Han Sans CN";
font-weight: 400;
/* Regular */
font-size: 14px;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
/* 左对齐 */
color: rgb(95, 101, 108);
}
}
.source-view-detail-content {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
color: rgb(95, 101, 108);
}
} }
...@@ -1631,7 +2125,7 @@ onMounted(async () => { ...@@ -1631,7 +2125,7 @@ onMounted(async () => {
width: 100%; width: 100%;
display: flex; display: flex;
padding-top: 4px; padding-top: 13px;
padding-left: 24px; padding-left: 24px;
padding-right: 24px; padding-right: 24px;
padding-bottom: 18px; padding-bottom: 18px;
...@@ -1641,14 +2135,17 @@ onMounted(async () => { ...@@ -1641,14 +2135,17 @@ onMounted(async () => {
.box2-main-Differences-content { .box2-main-Differences-content {
width: 1056px; width: 1056px;
overflow-y: auto; overflow-y: auto;
height: 848px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: hidden; overflow-x: hidden;
position: relative;
.Differences-item { .Differences-item {
width: 1056px; width: 1056px;
min-height: 62px; min-height: 62px;
flex-shrink: 0;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
...@@ -1661,6 +2158,39 @@ onMounted(async () => { ...@@ -1661,6 +2158,39 @@ onMounted(async () => {
} }
.empty-image {
height: 200px;
width: 200px;
position: absolute;
left: 428px;
bottom: 372px;
img {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
/* 防止图片变形 */
}
}
.empty-text {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0px;
text-align: center;
display: flex;
width: 289px;
height: 30px;
position: absolute;
justify-content: center;
left: 384px;
bottom: 330px;
color: rgb(132, 136, 142);
}
.Differences-item:last-child { .Differences-item:last-child {
border-bottom: 1px solid rgb(234, 236, 238); border-bottom: 1px solid rgb(234, 236, 238);
} }
...@@ -1690,8 +2220,8 @@ onMounted(async () => { ...@@ -1690,8 +2220,8 @@ onMounted(async () => {
} }
.Differences-item-title { .Differences-item-title {
width: 958px;
flex: 1;
min-width: 0; min-width: 0;
min-height: 30px; min-height: 30px;
margin-left: 18px; margin-left: 18px;
...@@ -1703,36 +2233,25 @@ onMounted(async () => { ...@@ -1703,36 +2233,25 @@ onMounted(async () => {
text-align: justify; text-align: justify;
color: rgb(59, 65, 75); color: rgb(59, 65, 75);
display: flex; display: flex;
gap: 36px
} }
.Differences-item:has(+ .Differences-expand) .Differences-item-title { .Differences-item:has(+ .Differences-expand) .Differences-item-title-left {
font-weight: 700; font-weight: 700;
} }
.Differences-item-title-right { .Differences-item-title-right {
min-height: 30px; min-height: 30px;
width: 461px; margin-left: 18px;
width: 89px;
} }
.Differences-item-title-left { .Differences-item-title-left {
min-height: 30px; min-height: 30px;
width: 461px; width: 845px;
} }
.Differences-item-right {
flex: 0 0 auto;
width: 89px;
height: 30px;
margin-left: 18px;
color: rgb(95, 101, 108);
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
}
.Differences-item-image { .Differences-item-image {
flex: 0 0 auto; flex: 0 0 auto;
...@@ -1749,12 +2268,13 @@ onMounted(async () => { ...@@ -1749,12 +2268,13 @@ onMounted(async () => {
.Differences-expand { .Differences-expand {
width: 1056px; width: 1056px;
flex-shrink: 0;
box-sizing: border-box; box-sizing: border-box;
border-top: none; border-top: none;
padding: 22px 50px 24px 58px; padding: 0px 50px 24px 58px;
display: flex; display: flex;
} }
...@@ -1775,14 +2295,14 @@ onMounted(async () => { ...@@ -1775,14 +2295,14 @@ onMounted(async () => {
} }
.Differences-expand-top-content-left { .Differences-expand-top-content-left {
width: 452px; width: 948px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.content-left-title { .content-left-title {
width: 452px; width: 948px;
height: 24px; height: 24px;
display: flex; display: flex;
overflow: hidden; overflow: hidden;
...@@ -1798,7 +2318,7 @@ onMounted(async () => { ...@@ -1798,7 +2318,7 @@ onMounted(async () => {
} }
.content-left-img-name { .content-left-img-name {
width: 452px; width: 948px;
height: 22px; height: 22px;
display: flex; display: flex;
margin-top: 8px; margin-top: 8px;
...@@ -1844,76 +2364,82 @@ onMounted(async () => { ...@@ -1844,76 +2364,82 @@ onMounted(async () => {
} }
.differences-source-views {
.content-right-title { .source-view-detail {
width: 452px; margin-top: 16px;
height: 24px; padding-bottom: 16px;
display: flex; border-bottom: 1px solid rgb(234, 236, 238);
}
/* 关键:超出宽度显示 ... */ .source-view-detail-title {
overflow: hidden; display: flex;
white-space: nowrap; align-items: flex-start;
text-overflow: ellipsis; justify-content: space-between;
color: rgb(59, 65, 75); gap: 16px;
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
font-weight: 400 !important; font-weight: 500;
font-size: 16px; font-size: 15px;
line-height: 24px; line-height: 24px;
letter-spacing: 0px; color: rgb(59, 65, 75);
text-align: justify; margin-bottom: 6px;
} }
.content-right-img-name { .source-view-detail-title-text {
width: 452px; justify-content: space-between;
height: 22px; font-family: "Source Han Sans CN", sans-serif;
display: flex; font-weight: 700;
margin-top: 8px; font-size: 16px;
line-height: 30px;
letter-spacing: 0;
text-align: justify;
} }
.content-right-img { .source-view-detail-org {
width: 16px; flex: 0 0 auto;
height: 16px; display: inline-flex;
margin-top: 3px; align-items: center;
margin-right: 4px; gap: 4px;
color: rgb(95, 101, 108);
font-size: 14px;
line-height: 22px;
margin-top: 4px;
img { img {
width: 100%; width: 16px;
height: 100%; height: 16px;
display: block; display: block;
}
}
.content-right-name { object-fit: cover;
margin-top: 3px;
}
height: 22px; .source-view-detail-org-text {
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
font-weight: 400; font-weight: 400;
/* Regular */
font-size: 14px; font-size: 14px;
line-height: 22px; line-height: 22px;
letter-spacing: 0; letter-spacing: 0px;
text-align: left; text-align: left;
/* 左对齐 */
color: rgb(95, 101, 108); color: rgb(95, 101, 108);
} }
.Differences-expand-top-content-right {
width: 452px;
display: flex;
flex-direction: column;
} }
.content-right-text { .source-view-detail-content {
font-family: "Source Han Sans CN"; font-family: "Source Han Sans CN";
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
line-height: 30px; line-height: 30px;
letter-spacing: 0px; letter-spacing: 0;
text-align: justify; text-align: justify;
color: rgb(95, 101, 108); color: rgb(95, 101, 108);
margin-top: 8px; }
} }
.Differences-expand-view { .Differences-expand-view {
width: 948px; width: 948px;
height: 36px; height: 36px;
...@@ -1945,28 +2471,6 @@ onMounted(async () => { ...@@ -1945,28 +2471,6 @@ onMounted(async () => {
justify-content: center; justify-content: center;
} }
.Differences-expand-view-right {
width: 452px;
height: 36px;
}
.right-tag {
width: 112px;
height: 28px;
color: rgb(5, 95, 194);
background-color: rgb(231, 243, 255);
border-radius: 4px;
margin-top: 8px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 16px;
line-height: 24px;
letter-spacing: 0px;
text-align: justify;
display: flex;
align-items: center;
justify-content: center;
}
} }
......
...@@ -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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论