From 64e93d25b88c8c81301f155fd9834ea2bbcd7466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?= Date: Fri, 24 Oct 2025 19:03:40 +0800 Subject: [PATCH] =?UTF-8?q?=20=20-=20=E5=9C=A8=20js/utils.js:24-79=20?= =?UTF-8?q?=E5=BC=BA=E5=8C=96=20parseSVGResponse=EF=BC=8C=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E7=BC=BA=E5=A4=B1=E7=BB=93=E5=B0=BE=E5=8F=8D=E5=BC=95?= =?UTF-8?q?=E5=8F=B7=E5=92=8C=E6=88=AA=E6=96=AD=E7=9A=84=20SVG=EF=BC=8C?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=A1=A5=E9=BD=90=20=20=E5=B9=B6?= =?UTF-8?q?=E5=8E=BB=E9=99=A4=E6=AE=8B=20=20=20=20=20=E7=95=99=E7=9A=84=20?= =?UTF-8?q?```=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=90=8E=E7=BB=AD=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E8=83=BD=E7=A8=B3=E5=AE=9A=E6=8F=90=E5=8F=96=E5=9B=BE?= =?UTF-8?q?=E5=BD=A2=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/apiclient.js | 8 +- js/app.js | 268 ++++++++++++++++++++++++------------------------ js/utils.js | 82 ++++++++++----- 3 files changed, 196 insertions(+), 162 deletions(-) diff --git a/js/apiclient.js b/js/apiclient.js index 5446aee..a6b7e5c 100644 --- a/js/apiclient.js +++ b/js/apiclient.js @@ -133,7 +133,7 @@ class APIClient { { role: 'user', content: userRequest } ]; - return await this.sendChatMessage(messages, { maxTokens: 3000 }); + return await this.sendChatMessage(messages, { maxTokens: 18000 }); } // 生成SWOT分析的专用方法 @@ -144,7 +144,7 @@ class APIClient { { role: 'user', content: userRequest } ]; - return await this.sendChatMessage(messages, { maxTokens: 3000 }); + return await this.sendChatMessage(messages, { maxTokens: 18000 }); } // 流式生成产品画布 @@ -155,7 +155,7 @@ class APIClient { { role: 'user', content: userRequest } ]; - return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete); + return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete); } // 流式生成SWOT分析 @@ -166,7 +166,7 @@ class APIClient { { role: 'user', content: userRequest } ]; - return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete); + return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete); } // 流式发送聊天请求 diff --git a/js/app.js b/js/app.js index 569ece4..1d4366b 100644 --- a/js/app.js +++ b/js/app.js @@ -320,14 +320,14 @@ class ProductCanvasApp { } }; - const onComplete = () => { - // 流式接收完成,处理完整消息 - this.finalizeStreamingMessage(messageId, fullContent, svgId, beforeText); - - this.isProcessing = false; - this.sendButton.disabled = false; - this.sendButton.innerHTML = ''; - }; + const onComplete = () => { + // 流式接收完成,处理完整消息 + this.finalizeStreamingMessage(messageId, fullContent, svgId); + + this.isProcessing = false; + this.sendButton.disabled = false; + this.sendButton.innerHTML = ''; + }; // 调用流式API if (this.currentMode === 'canvas') { @@ -394,11 +394,11 @@ class ProductCanvasApp { } } - // 更新SVG后的消息内容 - updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) { - // 使用Markdown解析文本 - const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); - const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); + // 更新SVG后的消息内容 + updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) { + // 使用Markdown解析文本 + const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); + const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); container.innerHTML = `
@@ -410,12 +410,36 @@ class ProductCanvasApp {
${parsedAfterText}
- `; - Utils.scrollToBottom(this.chatHistory); - } - - // 完成流式消息 - finalizeStreamingMessage(messageId, fullContent, svgId = null, beforeText = '') { + `; + Utils.scrollToBottom(this.chatHistory); + } + + // 组装标准化的SVG消息字符串 + buildSVGMessageContent(beforeText = '', svgBody = '', afterText = '') { + const segments = []; + const trimmedBefore = (beforeText || '').trim(); + const trimmedAfter = (afterText || '').trim(); + const trimmedSvg = (svgBody || '').trim(); + + if (trimmedBefore) { + segments.push(trimmedBefore); + } + + if (trimmedSvg) { + segments.push('```svg'); + segments.push(trimmedSvg); + segments.push('```'); + } + + if (trimmedAfter) { + segments.push(trimmedAfter); + } + + return segments.join('\n\n').trim(); + } + + // 完成流式消息 + finalizeStreamingMessage(messageId, fullContent, svgId = null) { let container = document.querySelector(`.chat-bubble-ai[data-message-id="${messageId}"]`); if (!container) { const fallback = document.querySelector(`[data-message-id="${messageId}"]`); @@ -426,121 +450,97 @@ class ProductCanvasApp { } } if (!container) return; - - const message = { - id: messageId, - type: 'ai', - content: fullContent, - timestamp: new Date().toISOString() - }; - - this.conversationHistory[this.currentMode].push(message); - - // 如果已经有SVG ID(从流式处理中获得),直接使用 - if (svgId && this.svgStorage[this.currentMode][svgId]) { - // 提取SVG后的文本 - let afterText = ''; - if (fullContent.includes('')) { - const svgEndIndex = fullContent.indexOf('') + 6; - afterText = fullContent.substring(svgEndIndex); - } - - // 使用Markdown解析文本 - const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); - const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); - - // 更新容器内容为包含SVG的消息 - container.innerHTML = ` -
- ${parsedBeforeText} -
- 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG -
- ${parsedAfterText} -
- -
- - -
- `; - } else { - // 使用原有的解析方法作为后备 - const parsed = Utils.parseSVGResponse(fullContent); - - // 如果包含SVG,存储SVG内容 - if (parsed.svgContent) { - const newSvgId = Utils.generateId('svg'); - this.svgStorage[this.currentMode][newSvgId] = { - content: parsed.svgContent, - messageId: messageId, - mode: this.currentMode, - timestamp: new Date().toISOString() - }; - - this.viewSVG(newSvgId); - - // 使用Markdown解析文本 - const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText); - const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText); - - // 更新容器内容为包含SVG的消息 - container.innerHTML = ` -
- ${parsedBeforeText} -
- 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG -
- ${parsedAfterText} -
- -
- - -
- `; - } else { - // 使用Markdown解析内容 - const parsedContent = typeof marked !== 'undefined' ? marked.parse(fullContent) : Utils.escapeHtml(fullContent); - - // 更新容器内容为普通消息 - container.innerHTML = ` -
- ${parsedContent} -
- -
- - -
- `; - } - } - - // 保存数据 - Utils.storage.set('canvasHistory', this.conversationHistory.canvas); - Utils.storage.set('swotHistory', this.conversationHistory.swot); - Utils.storage.set('canvasSVGs', this.svgStorage.canvas); - Utils.storage.set('swotSVGs', this.svgStorage.swot); - } + + const parsed = Utils.parseSVGResponse(fullContent); + const timestamp = new Date().toISOString(); + + const message = { + id: messageId, + type: 'ai', + content: '', + timestamp + }; + + container.classList.remove('streaming-text'); + container.setAttribute('data-message-id', messageId); + + const actionFooter = ` +
+ + +
+ `; + + if (parsed.svgContent && parsed.svgContent.includes('') + ? parsed.svgContent.trim() + : `${parsed.svgContent.trim()}\n`; + + let targetSvgId = svgId || null; + if (!targetSvgId || !this.svgStorage[this.currentMode][targetSvgId]) { + targetSvgId = targetSvgId || Utils.generateId('svg'); + } + + this.svgStorage[this.currentMode][targetSvgId] = { + content: svgBody, + messageId, + mode: this.currentMode, + timestamp + }; + + const beforeHtml = parsed.beforeText + ? (typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText)) + : ''; + const afterHtml = parsed.afterText + ? (typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText)) + : ''; + + container.innerHTML = ` +
+ ${beforeHtml} +
+ 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG +
+ ${afterHtml} +
+ ${actionFooter} + `; + + this.currentSvgId = targetSvgId; + this.svgViewer.innerHTML = svgBody; + + message.content = this.buildSVGMessageContent(parsed.beforeText, svgBody, parsed.afterText); + } else { + const sanitizedText = fullContent.replace(/^\s*```/, '').replace(/```$/, '').trim(); + const parsedContent = sanitizedText + ? (typeof marked !== 'undefined' ? marked.parse(sanitizedText) : Utils.escapeHtml(sanitizedText)) + : ''; + + container.innerHTML = ` +
+ ${parsedContent} +
+ ${actionFooter} + `; + + message.content = sanitizedText; + } + + this.conversationHistory[this.currentMode].push(message); + + // 保存数据 + Utils.storage.set('canvasHistory', this.conversationHistory.canvas); + Utils.storage.set('swotHistory', this.conversationHistory.swot); + Utils.storage.set('canvasSVGs', this.svgStorage.canvas); + Utils.storage.set('swotSVGs', this.svgStorage.swot); + Utils.scrollToBottom(this.chatHistory); + } // 清空当前对话 clearCurrentConversation() { diff --git a/js/utils.js b/js/utils.js index e2f15de..02b2b2b 100644 --- a/js/utils.js +++ b/js/utils.js @@ -21,29 +21,63 @@ function generateId(prefix = 'id') { return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } -// 解析SVG响应,提取SVG内容和前后文本 -function parseSVGResponse(response) { - const svgRegex = /```svg\s*([\s\S]*?)```/i; - const match = response.match(svgRegex); - - if (match) { - const svgContent = match[1].trim(); - const beforeText = response.substring(0, match.index).trim(); - const afterText = response.substring(match.index + match[0].length).trim(); - - return { - svgContent, - beforeText, - afterText - }; - } - - return { - svgContent: null, - beforeText: response, - afterText: '' - }; -} +// 解析SVG响应,提取SVG内容和前后文本,容错缺失的结束反引号 +function parseSVGResponse(response = '') { + const content = typeof response === 'string' ? response : String(response || ''); + const svgFenceRegex = /```(?:svg)?\s*([\s\S]*?)```/i; + const fenceMatch = content.match(svgFenceRegex); + + if (fenceMatch) { + const svgBody = fenceMatch[1].trim(); + const beforeText = content.substring(0, fenceMatch.index).trim(); + let afterText = content.substring(fenceMatch.index + fenceMatch[0].length).trim(); + afterText = afterText.replace(/^\s*```/, '').trim(); + + return { + svgContent: svgBody, + beforeText, + afterText + }; + } + + // 兼容缺失结束反引号的情况 + const svgStartRegex = /```(?:svg)?\s*'); + if (svgEndIndex !== -1) { + afterText = svgSection.substring(svgEndIndex + 6).replace(/```/, '').trim(); + svgSection = svgSection.substring(0, svgEndIndex + 6).trim(); + } + + // 补齐缺失的结束标签 + if (svgSection && !svgSection.endsWith('')) { + svgSection += '\n'; + } + + return { + svgContent: svgSection || null, + beforeText, + afterText + }; + } + + return { + svgContent: null, + beforeText: content.trim(), + afterText: '' + }; +} // 下载文件 function downloadFile(content, filename, mimeType = 'text/plain') { @@ -286,4 +320,4 @@ window.Utils = { autoResizeTextarea, StreamProcessor, createStreamRequest -}; \ No newline at end of file +};