- 在 js/utils.js:24-79 强化 parseSVGResponse,兼容缺失结尾反引号和截断的 SVG,自动补齐 </svg> 并去除残

留的 ```,确保后续渲染能稳定提取图形。
This commit is contained in:
史悦
2025-10-24 19:03:40 +08:00
parent bacafd66dc
commit 64e93d25b8
3 changed files with 196 additions and 162 deletions

View File

@@ -133,7 +133,7 @@ class APIClient {
{ role: 'user', content: userRequest } { role: 'user', content: userRequest }
]; ];
return await this.sendChatMessage(messages, { maxTokens: 3000 }); return await this.sendChatMessage(messages, { maxTokens: 18000 });
} }
// 生成SWOT分析的专用方法 // 生成SWOT分析的专用方法
@@ -144,7 +144,7 @@ class APIClient {
{ role: 'user', content: userRequest } { 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 } { role: 'user', content: userRequest }
]; ];
return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete); return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
} }
// 流式生成SWOT分析 // 流式生成SWOT分析
@@ -166,7 +166,7 @@ class APIClient {
{ role: 'user', content: userRequest } { role: 'user', content: userRequest }
]; ];
return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete); return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
} }
// 流式发送聊天请求 // 流式发送聊天请求

268
js/app.js
View File

@@ -320,14 +320,14 @@ class ProductCanvasApp {
} }
}; };
const onComplete = () => { const onComplete = () => {
// 流式接收完成,处理完整消息 // 流式接收完成,处理完整消息
this.finalizeStreamingMessage(messageId, fullContent, svgId, beforeText); this.finalizeStreamingMessage(messageId, fullContent, svgId);
this.isProcessing = false; this.isProcessing = false;
this.sendButton.disabled = false; this.sendButton.disabled = false;
this.sendButton.innerHTML = '<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>'; this.sendButton.innerHTML = '<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>';
}; };
// 调用流式API // 调用流式API
if (this.currentMode === 'canvas') { if (this.currentMode === 'canvas') {
@@ -394,11 +394,11 @@ class ProductCanvasApp {
} }
} }
// 更新SVG后的消息内容 // 更新SVG后的消息内容
updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) { updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) {
// 使用Markdown解析文本 // 使用Markdown解析文本
const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText);
const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText);
container.innerHTML = ` container.innerHTML = `
<div class="chat-bubble-ai relative group streaming-text" data-message-id="${container.dataset.messageId}"> <div class="chat-bubble-ai relative group streaming-text" data-message-id="${container.dataset.messageId}">
@@ -410,12 +410,36 @@ class ProductCanvasApp {
<div class="typing-cursor">${parsedAfterText}</div> <div class="typing-cursor">${parsedAfterText}</div>
</div> </div>
</div> </div>
`; `;
Utils.scrollToBottom(this.chatHistory); Utils.scrollToBottom(this.chatHistory);
} }
// 完成流式消息 // 组装标准化的SVG消息字符串
finalizeStreamingMessage(messageId, fullContent, svgId = null, beforeText = '') { 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}"]`); let container = document.querySelector(`.chat-bubble-ai[data-message-id="${messageId}"]`);
if (!container) { if (!container) {
const fallback = document.querySelector(`[data-message-id="${messageId}"]`); const fallback = document.querySelector(`[data-message-id="${messageId}"]`);
@@ -426,121 +450,97 @@ class ProductCanvasApp {
} }
} }
if (!container) return; if (!container) return;
const message = { const parsed = Utils.parseSVGResponse(fullContent);
id: messageId, const timestamp = new Date().toISOString();
type: 'ai',
content: fullContent, const message = {
timestamp: new Date().toISOString() id: messageId,
}; type: 'ai',
content: '',
this.conversationHistory[this.currentMode].push(message); timestamp
};
// 如果已经有SVG ID从流式处理中获得直接使用
if (svgId && this.svgStorage[this.currentMode][svgId]) { container.classList.remove('streaming-text');
// 提取SVG后的文本 container.setAttribute('data-message-id', messageId);
let afterText = '';
if (fullContent.includes('</svg>')) { const actionFooter = `
const svgEndIndex = fullContent.indexOf('</svg>') + 6; <div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
afterText = fullContent.substring(svgEndIndex); <button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${messageId}')">
} <iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
// 使用Markdown解析文本 </button>
const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); <button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${messageId}')">
const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); <iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
// 更新容器内容为包含SVG的消息 </button>
container.innerHTML = ` </div>
<div> `;
${parsedBeforeText}
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')"> if (parsed.svgContent && parsed.svgContent.includes('<svg')) {
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG const svgBody = parsed.svgContent.trim().endsWith('</svg>')
</div> ? parsed.svgContent.trim()
${parsedAfterText} : `${parsed.svgContent.trim()}\n</svg>`;
</div>
let targetSvgId = svgId || null;
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200"> if (!targetSvgId || !this.svgStorage[this.currentMode][targetSvgId]) {
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${messageId}')"> targetSvgId = targetSvgId || Utils.generateId('svg');
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon> }
<span>退回</span>
</button> this.svgStorage[this.currentMode][targetSvgId] = {
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${messageId}')"> content: svgBody,
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon> messageId,
<span>重新生成</span> mode: this.currentMode,
</button> timestamp
</div> };
`;
} else { const beforeHtml = parsed.beforeText
// 使用原有的解析方法作为后备 ? (typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText))
const parsed = Utils.parseSVGResponse(fullContent); : '';
const afterHtml = parsed.afterText
// 如果包含SVG存储SVG内容 ? (typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText))
if (parsed.svgContent) { : '';
const newSvgId = Utils.generateId('svg');
this.svgStorage[this.currentMode][newSvgId] = { container.innerHTML = `
content: parsed.svgContent, <div>
messageId: messageId, ${beforeHtml}
mode: this.currentMode, <div class="svg-placeholder-block" data-svg-id="${targetSvgId}" onclick="app.viewSVG('${targetSvgId}')">
timestamp: new Date().toISOString() 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
}; </div>
${afterHtml}
this.viewSVG(newSvgId); </div>
${actionFooter}
// 使用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); this.currentSvgId = targetSvgId;
this.svgViewer.innerHTML = svgBody;
// 更新容器内容为包含SVG的消息
container.innerHTML = ` message.content = this.buildSVGMessageContent(parsed.beforeText, svgBody, parsed.afterText);
<div> } else {
${parsedBeforeText} const sanitizedText = fullContent.replace(/^\s*```/, '').replace(/```$/, '').trim();
<div class="svg-placeholder-block" data-svg-id="${newSvgId}" onclick="app.viewSVG('${newSvgId}')"> const parsedContent = sanitizedText
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG ? (typeof marked !== 'undefined' ? marked.parse(sanitizedText) : Utils.escapeHtml(sanitizedText))
</div> : '';
${parsedAfterText}
</div> container.innerHTML = `
<div class="mb-1">
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200"> ${parsedContent}
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${messageId}')"> </div>
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon> ${actionFooter}
<span>退回</span> `;
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${messageId}')"> message.content = sanitizedText;
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon> }
<span>重新生成</span>
</button> this.conversationHistory[this.currentMode].push(message);
</div>
`; // 保存数据
} else { Utils.storage.set('canvasHistory', this.conversationHistory.canvas);
// 使用Markdown解析内容 Utils.storage.set('swotHistory', this.conversationHistory.swot);
const parsedContent = typeof marked !== 'undefined' ? marked.parse(fullContent) : Utils.escapeHtml(fullContent); Utils.storage.set('canvasSVGs', this.svgStorage.canvas);
Utils.storage.set('swotSVGs', this.svgStorage.swot);
// 更新容器内容为普通消息 Utils.scrollToBottom(this.chatHistory);
container.innerHTML = ` }
<div class="mb-1">
${parsedContent}
</div>
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${messageId}')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${messageId}')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
`;
}
}
// 保存数据
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);
}
// 清空当前对话 // 清空当前对话
clearCurrentConversation() { clearCurrentConversation() {

View File

@@ -21,29 +21,63 @@ function generateId(prefix = 'id') {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
} }
// 解析SVG响应提取SVG内容和前后文本 // 解析SVG响应提取SVG内容和前后文本,容错缺失的结束反引号
function parseSVGResponse(response) { function parseSVGResponse(response = '') {
const svgRegex = /```svg\s*([\s\S]*?)```/i; const content = typeof response === 'string' ? response : String(response || '');
const match = response.match(svgRegex); const svgFenceRegex = /```(?:svg)?\s*([\s\S]*?)```/i;
const fenceMatch = content.match(svgFenceRegex);
if (match) {
const svgContent = match[1].trim(); if (fenceMatch) {
const beforeText = response.substring(0, match.index).trim(); const svgBody = fenceMatch[1].trim();
const afterText = response.substring(match.index + match[0].length).trim(); const beforeText = content.substring(0, fenceMatch.index).trim();
let afterText = content.substring(fenceMatch.index + fenceMatch[0].length).trim();
return { afterText = afterText.replace(/^\s*```/, '').trim();
svgContent,
beforeText, return {
afterText svgContent: svgBody,
}; beforeText,
} afterText
};
return { }
svgContent: null,
beforeText: response, // 兼容缺失结束反引号的情况
afterText: '' const svgStartRegex = /```(?:svg)?\s*<svg[\s\S]*$/i;
}; const startMatch = content.match(svgStartRegex);
}
if (startMatch) {
const startIndex = startMatch.index;
const beforeText = content.substring(0, startIndex).trim();
let svgSection = content.substring(startIndex).replace(/```(?:svg)?\s*/i, '').trim();
// 去掉尾部残留的反引号
svgSection = svgSection.replace(/```$/, '').trim();
// 拆分 SVG 正文与额外文本
let afterText = '';
const svgEndIndex = svgSection.lastIndexOf('</svg>');
if (svgEndIndex !== -1) {
afterText = svgSection.substring(svgEndIndex + 6).replace(/```/, '').trim();
svgSection = svgSection.substring(0, svgEndIndex + 6).trim();
}
// 补齐缺失的结束标签
if (svgSection && !svgSection.endsWith('</svg>')) {
svgSection += '\n</svg>';
}
return {
svgContent: svgSection || null,
beforeText,
afterText
};
}
return {
svgContent: null,
beforeText: content.trim(),
afterText: ''
};
}
// 下载文件 // 下载文件
function downloadFile(content, filename, mimeType = 'text/plain') { function downloadFile(content, filename, mimeType = 'text/plain') {
@@ -286,4 +320,4 @@ window.Utils = {
autoResizeTextarea, autoResizeTextarea,
StreamProcessor, StreamProcessor,
createStreamRequest createStreamRequest
}; };