| | |
| | | :id="`ai-chart-${index}-${key}`" |
| | | ></div> |
| | | </div> |
| | | <div |
| | | v-else-if="message.chartMarkdownParseFailed" |
| | | class="chart-empty-state" |
| | | > |
| | | 图表解析失败,请稍后重试。 |
| | | </div> |
| | | |
| | | <!-- 表格内容 --> |
| | | <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper"> |
| | |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false, |
| | | localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] |
| | | } |
| | | |
| | |
| | | messageObj.purchaseData = null |
| | | messageObj.purchaseIntentData = null |
| | | messageObj.financeData = null |
| | | messageObj.chartMarkdownParseFailed = false |
| | | |
| | | if (isPurchaseIntentNotRecognized) { |
| | | messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData) |
| | |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false |
| | | }) |
| | | |
| | | outputState.value[botMsgIndex] = { |
| | |
| | | if (extracted) { |
| | | applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart) |
| | | } |
| | | currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex) |
| | | |
| | | // 最终解析确保图表渲染 |
| | | if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) { |
| | |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false |
| | | } |
| | | messages.value.push(botMsg) |
| | | |
| | |
| | | if (extracted) { |
| | | applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex) |
| | | } |
| | | currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex) |
| | | }).catch(err => { |
| | | if (err.name === 'CanceledError' || err.name === 'AbortError') { |
| | | console.log('Request aborted by user') |
| | |
| | | .replace(/</g, '<') |
| | | .replace(/>/g, '>') |
| | | .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 || '图表解析失败,请稍后重试。' |
| | | } |
| | | |
| | | const convertStreamOutput = (output, msgIndex) => { |
| | |
| | | } |
| | | } |
| | | |
| | | display = applyLocalChartMarkdownFallback(display, msgIndex) |
| | | let html = convertTextToHtml(display) |
| | | |
| | | // 还原代码块 |
| | |
| | | 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; |