From 2fd817ab50faa08111ce6e64c6d22a54807d08a4 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 09 六月 2026 15:16:48 +0800
Subject: [PATCH] pro 1.bom编辑页面样式优化
---
src/components/AIChatSidebar/index.vue | 587 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 531 insertions(+), 56 deletions(-)
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index 9836854..7b22d7b 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/src/components/AIChatSidebar/index.vue
@@ -227,6 +227,12 @@
:id="`ai-chart-${index}-${key}`"
></div>
</div>
+ <div
+ v-else-if="message.chartMarkdownParseFailed"
+ class="chart-empty-state"
+ >
+ 鍥捐〃瑙f瀽澶辫触锛岃绋嶅悗閲嶈瘯銆�
+ </div>
<!-- 琛ㄦ牸鍐呭 -->
<div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
@@ -486,6 +492,114 @@
</template>
</el-table-column>
</el-table>
+ </div>
+ </div>
+
+ <div v-if="message.purchaseData" class="sales-structured-card">
+ <div class="sales-structured-card__title">{{ getPurchaseTypeLabel(message.type) }}</div>
+
+ <div v-if="message.purchaseData.summaryEntries?.length" class="sales-summary-grid">
+ <div
+ v-for="(entry, entryIndex) in message.purchaseData.summaryEntries"
+ :key="`purchase-summary-${entry.key}-${entryIndex}`"
+ class="sales-summary-item"
+ >
+ <span class="sales-summary-label">{{ entry.label }}</span>
+ <strong class="sales-summary-value">{{ entry.value }}</strong>
+ </div>
+ </div>
+
+ <div
+ v-if="message.purchaseData.listItems?.length && message.purchaseData.columns?.length"
+ class="table-wrapper manufacturing-table-wrapper"
+ >
+ <el-table :data="message.purchaseData.listItems" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.purchaseData.columns"
+ :key="col"
+ :label="getStructuredFieldLabel(col)"
+ min-width="140"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div v-if="message.financeData" class="sales-structured-card">
+ <div class="sales-structured-card__title">{{ getFinancialTypeLabel(message.type) }}</div>
+
+ <div v-if="message.financeData.summaryEntries?.length" class="sales-summary-grid">
+ <div
+ v-for="(entry, entryIndex) in message.financeData.summaryEntries"
+ :key="`finance-summary-${entry.key}-${entryIndex}`"
+ class="sales-summary-item"
+ >
+ <span class="sales-summary-label">{{ entry.label }}</span>
+ <strong class="sales-summary-value">{{ entry.value }}</strong>
+ </div>
+ </div>
+
+ <div v-if="message.financeData.headline" class="finance-headline">
+ {{ message.financeData.headline }}
+ </div>
+
+ <div v-if="message.financeData.conclusions?.length" class="finance-text-section">
+ <div class="sales-section-title">鏍稿績缁撹</div>
+ <ul>
+ <li v-for="(item, idx) in message.financeData.conclusions" :key="`finance-conclusion-${idx}`">
+ {{ item }}
+ </li>
+ </ul>
+ </div>
+
+ <div v-if="message.financeData.riskSuggestions?.length" class="finance-text-section">
+ <div class="sales-section-title">椋庨櫓寤鸿</div>
+ <ul>
+ <li v-for="(item, idx) in message.financeData.riskSuggestions" :key="`finance-risk-${idx}`">
+ {{ item }}
+ </li>
+ </ul>
+ </div>
+
+ <div
+ v-for="section in message.financeData.sections"
+ :key="`finance-section-${section.key}`"
+ class="table-wrapper manufacturing-table-wrapper"
+ >
+ <div class="sales-section-title">{{ section.title }}</div>
+ <el-table :data="section.items" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in section.columns"
+ :key="`finance-${section.key}-${col}`"
+ :label="getStructuredFieldLabel(col)"
+ min-width="120"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div v-if="message.purchaseIntentData?.quickPrompts?.length" class="purchase-intent-quick-prompt-wrap">
+ <div class="purchase-intent-quick-prompt-title">璇曡瘯浠ヤ笅鎻愰棶</div>
+ <div class="quick-prompt-list purchase-intent-quick-prompt-list">
+ <button
+ v-for="prompt in message.purchaseIntentData.quickPrompts"
+ :key="`purchase-intent-${prompt}`"
+ type="button"
+ class="quick-prompt-btn"
+ :disabled="isSending"
+ @click="sendQuickPrompt(prompt)"
+ >
+ {{ prompt }}
+ </button>
</div>
</div>
@@ -899,12 +1013,37 @@
sales_customer_profile_list: '瀹㈡埛妗f',
sales_quotation_list: '閿�鍞姤浠�',
sales_ledger_list: '閿�鍞彴璐�',
- sales_return_list: '閿�鍞��璐�',
+ sales_return_list: '閿�鍞��娆�/鍥炴璁板綍',
sales_customer_interaction_list: '瀹㈡埛寰�鏉�',
sales_shipping_list: '鍙戣揣鍙拌处',
sales_dashboard: '閿�鍞寚鏍囩粺璁�',
sales_customer_churn_risk: '瀹㈡埛娴佸け椋庨櫓鍒嗘瀽',
sales_collection_quote_strategy: '鍥炴涓庢姤浠风瓥鐣ュ缓璁�'
+}
+const purchaseTypeLabelMap = {
+ purchase_material_rank: '閲囪喘鐗╂枡閲戦鎺掕',
+ purchase_stats: '閲囪喘缁熻',
+ purchase_pending_payment_list: '寰呬粯娆鹃噰璐崟'
+}
+const financialStructuredTypeSet = new Set([
+ 'financial_cost_accounting',
+ 'financial_order_profit_analysis',
+ 'financial_inventory_capital_analysis',
+ 'financial_cashflow_forecast',
+ 'financial_business_anomaly_warning',
+ 'financial_business_cockpit',
+ 'financial_operation_report',
+ 'financial_rag_knowledge'
+])
+const financialTypeLabelMap = {
+ financial_cost_accounting: '鎴愭湰鏍哥畻',
+ financial_order_profit_analysis: '璁㈠崟鍒╂鼎鍒嗘瀽',
+ financial_inventory_capital_analysis: '搴撳瓨璧勯噾鍒嗘瀽',
+ financial_cashflow_forecast: '鐜伴噾娴侀娴�',
+ financial_business_anomaly_warning: '缁忚惀寮傚父棰勮',
+ financial_business_cockpit: '缁忚惀椹鹃┒鑸�',
+ financial_operation_report: '缁忚惀鎶ュ憡',
+ financial_rag_knowledge: '璐㈠姟鐭ヨ瘑妫�绱�'
}
const manufacturingStructuredTypeSet = new Set([
'manufacturing_site_snapshot',
@@ -976,6 +1115,26 @@
stockQty: '搴撳瓨閲�'
}
Object.assign(structuredFieldLabelMap, {
+ refundId: '閫�娆�/鍥炴鍗曞彿',
+ collectionNumber: '鏀舵鍗曞彿',
+ collectionAmount: '鏀舵閲戦',
+ actualAmount: '鏀舵閲戦(鍏煎)',
+ customerId: '瀹㈡埛ID',
+ salesLedgerId: '閿�鍞彴璐D',
+ projectName: '椤圭洰鍚嶇О',
+ receiptPaymentDate: '鏀朵粯娆炬棩鏈�',
+ receiptPaymentAmount: '鏀朵粯娆鹃噾棰�',
+ receiptPaymentType: '鏀朵粯娆剧被鍨�',
+ registrant: '鐧昏浜�',
+ returnAmount: '鍥炴閲戦姹囨��',
+ totalReceiptAmount: '鏀舵鎬婚',
+ customerCount: '瀹㈡埛鏁�',
+ paidAmount: '宸蹭粯娆鹃噾棰�',
+ contractAmount: '鍚堝悓閲戦',
+ paymentCount: '浠樻绗旀暟',
+ invoiceCount: '鍙戠エ绗旀暟',
+ paymentAmount: '浠樻閲戦',
+ invoiceAmount: '鍙戠エ閲戦',
customerName: '瀹㈡埛鍚嶇О',
riskLevel: '椋庨櫓绛夌骇',
riskScore: '椋庨櫓璇勫垎',
@@ -988,10 +1147,40 @@
collectionStrategy: '鍥炴绛栫暐',
quotationStrategy: '鎶ヤ环绛栫暐',
nextAction: '涓嬩竴姝ュ姩浣�',
+ salesContractNo: '閿�鍞悎鍚屽彿',
+ revenue: '鏀跺叆',
+ materialCost: '鏉愭枡鎴愭湰',
+ laborCost: '浜哄伐鎴愭湰',
+ depreciationCost: '鎶樻棫鎴愭湰',
+ scrapCost: '鎶ュ簾鎴愭湰',
+ totalCost: '鎬绘垚鏈�',
+ profit: '鍒╂鼎',
+ profitRate: '鍒╂鼎鐜�',
+ reasons: '鍘熷洜',
+ suggestion: '寤鸿',
+ productName: '浜у搧鍚嶇О',
+ model: '鍨嬪彿',
+ quantity: '鏁伴噺',
+ inventoryValue: '搴撳瓨璧勯噾',
+ stagnantDays: '鍛嗘粸澶╂暟',
+ overstock: '鏄惁瓒呭偍',
+ month: '鏈堜唤',
+ income: '鏀跺叆',
+ expense: '鏀嚭',
+ netFlow: '鍑�鐜伴噾娴�',
+ message: '棰勮淇℃伅',
+ headline: '鎶ュ憡鏍囬',
+ conclusions: '鏍稿績缁撹',
+ riskSuggestions: '椋庨櫓寤鸿',
+ orderProfitTop: '鍒╂鼎Top',
contractAmountTotal: '鍚堝悓鎬婚',
receivedAmountTotal: '宸插洖娆鹃噾棰�',
pendingAmountTotal: '寰呭洖娆炬�婚',
- shipRate: '鍙戣揣鐜�'
+ shipRate: '鍙戣揣鐜�',
+ pendingOrderCount: '寰呬粯娆捐鍗曟暟',
+ totalContractAmount: '寰呬粯娆惧悎鍚屾�婚',
+ totalPaidAmount: '宸蹭粯娆炬�婚',
+ totalPendingAmount: '寰呬粯娆炬�婚'
})
const purchasePayloadFieldLabelMap = {
purchaseLedgers: '閲囪喘鍙拌处',
@@ -1392,12 +1581,41 @@
return items.filter(item => isPlainObject(item))
}
+const normalizeSalesReturnListItems = (items = []) => {
+ return normalizeSalesListItems(items).map((item) => {
+ const collectionAmount = item.collectionAmount ?? item.actualAmount
+ const collectionNumber = item.collectionNumber || item.refundId || ''
+ return {
+ ...item,
+ refundId: item.refundId || collectionNumber,
+ collectionNumber,
+ collectionAmount: collectionAmount ?? 0,
+ actualAmount: item.actualAmount ?? collectionAmount ?? 0
+ }
+ })
+}
+
+const normalizeSalesTypeListItems = (type = '', items = []) => {
+ if (type === 'sales_return_list') {
+ return normalizeSalesReturnListItems(items)
+ }
+ return normalizeSalesListItems(items)
+}
+
+const inferSalesColumnsByType = (type = '', listItems = []) => {
+ const columns = inferSalesColumns(listItems)
+ if (type === 'sales_return_list') {
+ return columns.filter(column => column !== 'actualAmount')
+ }
+ return columns
+}
+
const buildSalesStructuredData = (parsedData) => {
const type = String(parsedData?.type || '')
if (!salesStructuredTypeSet.has(type)) return null
const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
- const listItems = normalizeSalesListItems(rawData.items)
+ const listItems = normalizeSalesTypeListItems(type, rawData.items)
const topCustomers = normalizeSalesListItems(rawData.topCustomers)
const contractTrend = normalizeSalesListItems(rawData.contractTrend)
@@ -1405,7 +1623,7 @@
type,
summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
listItems,
- columns: inferSalesColumns(listItems),
+ columns: inferSalesColumnsByType(type, listItems),
topCustomers,
topCustomerColumns: inferSalesColumns(topCustomers),
contractTrend,
@@ -1414,6 +1632,69 @@
}
const getSalesTypeLabel = (type = '') => salesTypeLabelMap[String(type || '')] || '閿�鍞煡璇㈢粨鏋�'
+
+const buildPurchaseStructuredData = (parsedData) => {
+ if (parsedData?.success !== true) return null
+
+ const type = String(parsedData?.type || '')
+ if (!type.startsWith('purchase_')) return null
+
+ const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
+ const listItems = normalizeSalesListItems(rawData.items)
+
+ return {
+ type,
+ summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
+ listItems,
+ columns: inferSalesColumns(listItems)
+ }
+}
+
+const getPurchaseTypeLabel = (type = '') => purchaseTypeLabelMap[String(type || '')] || '閲囪喘鏌ヨ缁撴灉'
+
+const financialDataSectionConfig = [
+ { key: 'orders', title: '璁㈠崟鏄庣粏' },
+ { key: 'items', title: '鏄庣粏鍒楄〃' },
+ { key: 'actualMonthly', title: '鍘嗗彶鐜伴噾娴�' },
+ { key: 'forecastMonthly', title: '棰勬祴鐜伴噾娴�' },
+ { key: 'receivableRiskTop', title: '搴旀敹椋庨櫓Top' },
+ { key: 'payablePressureTop', title: '搴斾粯鍘嬪姏Top' },
+ { key: 'orderProfitTop', title: '鍒╂鼎Top' }
+]
+
+const buildFinancialStructuredSections = (rawData = {}) => {
+ const sections = []
+ financialDataSectionConfig.forEach((section) => {
+ const items = normalizeSalesListItems(rawData?.[section.key])
+ if (!items.length) return
+ sections.push({
+ key: section.key,
+ title: section.title,
+ items,
+ columns: inferSalesColumns(items)
+ })
+ })
+ return sections
+}
+
+const buildFinancialStructuredData = (parsedData) => {
+ const type = String(parsedData?.type || '')
+ if (!financialStructuredTypeSet.has(type) || parsedData?.success !== true) return null
+
+ const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
+ const headline = String(rawData?.headline || '').trim()
+
+ return {
+ type,
+ summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
+ headline,
+ conclusions: toStructuredStringArray(rawData?.conclusions),
+ riskSuggestions: toStructuredStringArray(rawData?.riskSuggestions),
+ sections: buildFinancialStructuredSections(rawData)
+ }
+}
+
+const getFinancialTypeLabel = (type = '') => financialTypeLabelMap[String(type || '')] || '璐㈠姟鏌ヨ缁撴灉'
const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || ''))
@@ -1446,6 +1727,13 @@
.filter(Boolean)
}
return []
+}
+
+const normalizePurchaseIntentNotRecognizedData = (parsedData) => {
+ if (String(parsedData?.type || '') !== 'purchase_intent_not_recognized') return null
+ const quickPrompts = toStructuredStringArray(parsedData?.data?.quickPrompts)
+ if (!quickPrompts.length) return null
+ return { quickPrompts }
}
const getManufacturingWarningLevelType = (level = '') => {
@@ -1827,6 +2115,10 @@
purchaseAnalysisData: null,
manufacturingData: null,
salesData: null,
+ purchaseData: null,
+ purchaseIntentData: null,
+ financeData: null,
+ chartMarkdownParseFailed: false,
localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
}
@@ -2045,7 +2337,7 @@
const candidate = text.slice(i, j + 1)
try {
const parsed = JSON.parse(candidate)
- if (parsed?.success === true) {
+ if (typeof parsed?.success === 'boolean') {
return {
data: parsed,
startIdx: i,
@@ -2064,7 +2356,8 @@
}
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
- if (!messageObj || !parsedData?.success) return
+ const isPurchaseIntentNotRecognized = String(parsedData?.type || '') === 'purchase_intent_not_recognized'
+ if (!messageObj || (parsedData?.success !== true && !isPurchaseIntentNotRecognized)) return
const previousManufacturingData = messageObj.manufacturingData
messageObj.type = parsedData.type || ''
@@ -2072,6 +2365,17 @@
messageObj.purchaseAnalysisData = null
messageObj.manufacturingData = null
messageObj.salesData = null
+ messageObj.purchaseData = null
+ messageObj.purchaseIntentData = null
+ messageObj.financeData = null
+ messageObj.chartMarkdownParseFailed = false
+
+ if (isPurchaseIntentNotRecognized) {
+ messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
+ messageObj.chartOptions = null
+ messageObj.chartRenderReady = false
+ return
+ }
if (messageObj.type === 'todo_list' && parsedData.data) {
messageObj.tableData = parsedData.data
@@ -2087,11 +2391,23 @@
messageObj.manufacturingData = manufacturingData
}
+ const purchaseData = buildPurchaseStructuredData(parsedData)
+ if (purchaseData) {
+ messageObj.purchaseData = purchaseData
+ }
+
+ const financeData = buildFinancialStructuredData(parsedData)
+ if (financeData) {
+ messageObj.financeData = financeData
+ }
+
if (parsedData.action === 'confirm_required' && parsedData.businessType) {
messageObj.type = 'purchase_analysis_confirm'
messageObj.purchaseAnalysisData = parsedData
messageObj.manufacturingData = null
messageObj.salesData = null
+ messageObj.purchaseData = null
+ messageObj.financeData = null
if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
initializePurchasePayloadTree(messageObj, parsedData.payload || {})
}
@@ -2136,10 +2452,13 @@
const getStructuredFallbackText = (parsedData) => {
if (!parsedData) return '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
if (parsedData.type === 'todo_list') return '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁銆�'
+ if (parsedData.type === 'purchase_intent_not_recognized') return '鏈瘑鍒埌鍙墽琛岀殑閲囪喘鏌ヨ鏉′欢锛岃琛ュ厖鏌ヨ鏉′欢鍚庡啀璇曘��'
if (salesStructuredTypeSet.has(parsedData.type)) {
if (parsedData.type === 'sales_customer_churn_risk') return '宸蹭负鎮ㄧ敓鎴愬鎴锋祦澶遍闄╁垎鏋愩��'
if (parsedData.type === 'sales_collection_quote_strategy') return '宸蹭负鎮ㄧ敓鎴愬洖娆句笌鎶ヤ环绛栫暐寤鸿銆�'
if (parsedData.type === 'sales_dashboard') return '宸蹭负鎮ㄧ敓鎴愰攢鍞寚鏍囩粺璁°��'
+ if (parsedData.type === 'sales_return_list') return '宸茶繑鍥為攢鍞��娆�/鍥炴璁板綍銆�'
+ if (parsedData.type === 'sales_customer_interaction_list') return '宸茶繑鍥炲鎴峰線鏉ユ暟鎹��'
return '宸茶繑鍥為攢鍞煡璇㈢粨鏋溿��'
}
if (manufacturingStructuredTypeSet.has(parsedData.type)) {
@@ -2148,8 +2467,31 @@
if (parsedData.type === 'manufacturing_analysis') return '宸蹭负鎮ㄧ敓鎴愬埗閫犲垎鏋愮粨鏋溿��'
return '宸茶繑鍥炲埗閫犳煡璇㈢粨鏋溿��'
}
+ if (financialStructuredTypeSet.has(parsedData.type)) return '宸茶繑鍥炶储鍔″垎鏋愮粨鏋溿��'
+ if (String(parsedData.type || '').startsWith('purchase_')) return '宸茶繑鍥為噰璐煡璇㈢粨鏋溿��'
if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
return '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+}
+
+const resolveStructuredDescription = (parsedData) => {
+ if (!parsedData) return ''
+ if (parsedData.action === 'confirm_required') {
+ return getPurchaseConfirmDescription(parsedData)
+ }
+
+ const description = String(parsedData.description || '').trim()
+ if (!description) return ''
+
+ if (parsedData.type === 'sales_customer_interaction_list') {
+ if (description === 'no_customer_interactions') return '璇ユ椂闂磋寖鍥存殏鏃犲鎴峰線鏉ヨ褰曘��'
+ if (description === 'ok') return '宸茶繑鍥炲鎴峰線鏉ユ暟鎹��'
+ }
+
+ if (description === 'ok' || description === 'success') {
+ return getStructuredFallbackText(parsedData)
+ }
+
+ return description
}
const buildPurchaseMaterialRankCharts = (parsedData) => {
@@ -3278,7 +3620,11 @@
payloadHiddenData: null,
purchaseAnalysisData: null,
manufacturingData: null,
- salesData: null
+ salesData: null,
+ purchaseData: null,
+ purchaseIntentData: null,
+ financeData: null,
+ chartMarkdownParseFailed: false
})
outputState.value[botMsgIndex] = {
@@ -3334,6 +3680,7 @@
if (extracted) {
applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
}
+ currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
// 鏈�缁堣В鏋愮‘淇濆浘琛ㄦ覆鏌�
if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
@@ -3404,7 +3751,11 @@
payloadHiddenData: null,
purchaseAnalysisData: null,
manufacturingData: null,
- salesData: null
+ salesData: null,
+ purchaseData: null,
+ purchaseIntentData: null,
+ financeData: null,
+ chartMarkdownParseFailed: false
}
messages.value.push(botMsg)
@@ -3436,28 +3787,6 @@
const extracted = extractEmbeddedSuccessJson(fullText)
if (extracted) {
applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
- } else {
- const extractJson = (text) => {
- const startIdx = text.indexOf('{"success": true')
- if (startIdx === -1) return null
-
- // 浠庡悗寰�鍓嶆壘鏈�鍚庝竴涓� '}'
- const lastBraceIdx = text.lastIndexOf('}')
- if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
-
- const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
- try {
- return JSON.parse(potentialJson)
- } catch (err) {
- return null
- }
- }
-
- const parsedData = extractJson(fullText)
- if (parsedData) {
- applyStructuredMessageData(currentMsg, parsedData, botMsgIndex, true)
- }
-
}
updateOutputState(fullText, botMsgIndex)
@@ -3475,25 +3804,8 @@
const extracted = extractEmbeddedSuccessJson(currentMsg.content)
if (extracted) {
applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
- } else {
- const extractJson = (text) => {
- const startIdx = text.indexOf('{"success": true')
- if (startIdx === -1) return null
- const lastBraceIdx = text.lastIndexOf('}')
- if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
- const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
- try {
- return JSON.parse(potentialJson)
- } catch (err) {
- return null
- }
- }
-
- const finalParsed = extractJson(currentMsg.content)
- if (finalParsed) {
- applyStructuredMessageData(currentMsg, finalParsed, botMsgIndex)
- }
}
+ currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
}).catch(err => {
if (err.name === 'CanceledError' || err.name === 'AbortError') {
console.log('Request aborted by user')
@@ -3537,6 +3849,126 @@
.replace(/\n/g, '<br>')
}
+const localChartMarkdownImagePattern = /!\[[^\]]*]\((https?:\/\/local\/generate_chart\?[^)\s]+)\)/gi
+
+const parseLocalChartOptionText = (optionText = '') => {
+ const text = String(optionText || '').trim()
+ if (!text) return null
+
+ const parseCandidates = [text]
+ try {
+ const decoded = decodeURIComponent(text)
+ if (decoded && decoded !== text) {
+ parseCandidates.push(decoded)
+ }
+ } catch (err) {
+ // Keep original text candidate.
+ }
+
+ for (const candidate of parseCandidates) {
+ try {
+ const parsed = JSON.parse(candidate)
+ if (isPlainObject(parsed)) {
+ return parsed
+ }
+ } catch (err) {
+ continue
+ }
+ }
+
+ return null
+}
+
+const parseLocalChartOptionFromUrl = (urlText = '') => {
+ try {
+ const url = new URL(String(urlText || '').trim())
+ if (String(url.hostname || '').toLowerCase() !== 'local' || !String(url.pathname || '').includes('/generate_chart')) {
+ return null
+ }
+ const optionText = url.searchParams.get('options')
+ return parseLocalChartOptionText(optionText)
+ } catch (err) {
+ return null
+ }
+}
+
+const extractLocalChartMarkdown = (text = '') => {
+ const sourceText = String(text || '')
+ if (!sourceText) {
+ return {
+ cleanedText: '',
+ hasLocalChartMarkdown: false,
+ chartOptions: null,
+ parseFailed: false
+ }
+ }
+
+ let hasLocalChartMarkdown = false
+ let chartIndex = 0
+ const chartOptions = {}
+
+ const cleanedText = sourceText.replace(localChartMarkdownImagePattern, (fullMatch, chartUrl) => {
+ hasLocalChartMarkdown = true
+ const option = parseLocalChartOptionFromUrl(chartUrl)
+ if (option) {
+ chartOptions[`markdownChart_${chartIndex++}`] = option
+ }
+ return ''
+ })
+
+ const normalizedText = cleanedText
+ .replace(/\n[ \t]*\n[ \t]*\n+/g, '\n\n')
+ .trim()
+ const hasParsedCharts = Object.keys(chartOptions).length > 0
+
+ return {
+ cleanedText: normalizedText,
+ hasLocalChartMarkdown,
+ chartOptions: hasParsedCharts ? chartOptions : null,
+ parseFailed: hasLocalChartMarkdown && !hasParsedCharts
+ }
+}
+
+const applyLocalChartMarkdownFallback = (displayText, msgIndex) => {
+ const messageObj = messages.value[msgIndex]
+ if (!messageObj || messageObj.isUser) return displayText
+
+ const {
+ cleanedText,
+ hasLocalChartMarkdown,
+ chartOptions,
+ parseFailed
+ } = extractLocalChartMarkdown(displayText)
+
+ if (!hasLocalChartMarkdown) {
+ return displayText
+ }
+
+ if (chartOptions) {
+ messageObj.chartOptions = chartOptions
+ messageObj.chartRenderReady = true
+ messageObj.chartMarkdownParseFailed = false
+
+ const streamState = outputState.value[msgIndex]
+ if (!streamState || !streamState.hasRenderedChart) {
+ renderCharts(msgIndex, chartOptions)
+ if (streamState) {
+ streamState.hasRenderedChart = true
+ }
+ }
+
+ return cleanedText || '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
+ }
+
+ if (!messageObj.chartOptions || !messageObj.chartRenderReady) {
+ messageObj.chartOptions = null
+ messageObj.chartRenderReady = false
+ messageObj.chartMarkdownParseFailed = parseFailed
+ }
+
+ return cleanedText || '鍥捐〃瑙f瀽澶辫触锛岃绋嶅悗閲嶈瘯銆�'
+}
+
const convertStreamOutput = (output, msgIndex) => {
if (!output) return ''
const state = outputState.value[msgIndex]
@@ -3563,10 +3995,9 @@
display = ''
}
- if (parsed.description) {
- display = parsed.action === 'confirm_required'
- ? getPurchaseConfirmDescription(parsed)
- : parsed.description
+ const resolvedDescription = resolveStructuredDescription(parsed)
+ if (resolvedDescription) {
+ display = resolvedDescription
}
if (!display) {
@@ -3585,10 +4016,9 @@
display = ''
}
- if (parsed.description) {
- display = parsed.action === 'confirm_required'
- ? getPurchaseConfirmDescription(parsed)
- : parsed.description
+ const resolvedDescription = resolveStructuredDescription(parsed)
+ if (resolvedDescription) {
+ display = resolvedDescription
}
if (!display) {
@@ -3604,6 +4034,7 @@
}
}
+ display = applyLocalChartMarkdownFallback(display, msgIndex)
let html = convertTextToHtml(display)
// 杩樺師浠g爜鍧�
@@ -4586,6 +5017,18 @@
margin-bottom: 12px;
}
+.chart-empty-state {
+ margin-top: 12px;
+ width: 100%;
+ border-radius: 10px;
+ border: 1px dashed rgba(148, 163, 184, 0.6);
+ background: #f8fafc;
+ color: #64748b;
+ font-size: 13px;
+ line-height: 1.6;
+ padding: 12px;
+}
+
.table-wrapper {
margin-top: 12px;
background: #fff;
@@ -4889,6 +5332,38 @@
color: $deep-blue;
}
+.finance-headline {
+ margin-top: 4px;
+ font-size: 13px;
+ line-height: 1.7;
+ color: #344054;
+}
+
+.finance-text-section {
+ margin-top: 10px;
+
+ ul {
+ margin: 6px 0 0;
+ padding-left: 18px;
+ font-size: 13px;
+ line-height: 1.7;
+ color: #344054;
+ }
+}
+
+.purchase-intent-quick-prompt-wrap {
+ margin-top: 12px;
+}
+
+.purchase-intent-quick-prompt-title {
+ font-size: 12px;
+ color: #4b5563;
+}
+
+.purchase-intent-quick-prompt-list {
+ margin-top: 8px;
+}
+
.purchase-confirm-card {
margin-top: 12px;
width: 100%;
--
Gitblit v1.9.3