- 在 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);
} }
// 流式发送聊天请求 // 流式发送聊天请求

180
js/app.js
View File

@@ -322,7 +322,7 @@ 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;
@@ -414,8 +414,32 @@ class ProductCanvasApp {
Utils.scrollToBottom(this.chatHistory); 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, beforeText = '') { 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}"]`);
@@ -427,119 +451,95 @@ class ProductCanvasApp {
} }
if (!container) return; if (!container) return;
const parsed = Utils.parseSVGResponse(fullContent);
const timestamp = new Date().toISOString();
const message = { const message = {
id: messageId, id: messageId,
type: 'ai', type: 'ai',
content: fullContent, content: '',
timestamp: new Date().toISOString() timestamp
}; };
this.conversationHistory[this.currentMode].push(message); container.classList.remove('streaming-text');
container.setAttribute('data-message-id', messageId);
// 如果已经有SVG ID从流式处理中获得直接使用 const actionFooter = `
if (svgId && this.svgStorage[this.currentMode][svgId]) { <div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
// 提取SVG后的文本 <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}')">
let afterText = ''; <iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
if (fullContent.includes('</svg>')) { <span>退回</span>
const svgEndIndex = fullContent.indexOf('</svg>') + 6; </button>
afterText = fullContent.substring(svgEndIndex); <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>
`;
if (parsed.svgContent && parsed.svgContent.includes('<svg')) {
const svgBody = parsed.svgContent.trim().endsWith('</svg>')
? parsed.svgContent.trim()
: `${parsed.svgContent.trim()}\n</svg>`;
let targetSvgId = svgId || null;
if (!targetSvgId || !this.svgStorage[this.currentMode][targetSvgId]) {
targetSvgId = targetSvgId || Utils.generateId('svg');
} }
// 使用Markdown解析文本 this.svgStorage[this.currentMode][targetSvgId] = {
const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); content: svgBody,
const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); 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))
: '';
// 更新容器内容为包含SVG的消息
container.innerHTML = ` container.innerHTML = `
<div> <div>
${parsedBeforeText} ${beforeHtml}
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')"> <div class="svg-placeholder-block" data-svg-id="${targetSvgId}" onclick="app.viewSVG('${targetSvgId}')">
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
</div> </div>
${parsedAfterText} ${afterHtml}
</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> </div>
${actionFooter}
`; `;
this.currentSvgId = targetSvgId;
this.svgViewer.innerHTML = svgBody;
message.content = this.buildSVGMessageContent(parsed.beforeText, svgBody, parsed.afterText);
} else { } else {
// 使用原有的解析方法作为后备 const sanitizedText = fullContent.replace(/^\s*```/, '').replace(/```$/, '').trim();
const parsed = Utils.parseSVGResponse(fullContent); const parsedContent = sanitizedText
? (typeof marked !== 'undefined' ? marked.parse(sanitizedText) : Utils.escapeHtml(sanitizedText))
: '';
// 如果包含SVG存储SVG内容 container.innerHTML = `
if (parsed.svgContent) { <div class="mb-1">
const newSvgId = Utils.generateId('svg'); ${parsedContent}
this.svgStorage[this.currentMode][newSvgId] = { </div>
content: parsed.svgContent, ${actionFooter}
messageId: messageId, `;
mode: this.currentMode,
timestamp: new Date().toISOString()
};
this.viewSVG(newSvgId); message.content = sanitizedText;
// 使用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 = `
<div>
${parsedBeforeText}
<div class="svg-placeholder-block" data-svg-id="${newSvgId}" onclick="app.viewSVG('${newSvgId}')">
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
</div>
${parsedAfterText}
</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>
`;
} else {
// 使用Markdown解析内容
const parsedContent = typeof marked !== 'undefined' ? marked.parse(fullContent) : Utils.escapeHtml(fullContent);
// 更新容器内容为普通消息
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>
`;
}
} }
this.conversationHistory[this.currentMode].push(message);
// 保存数据 // 保存数据
Utils.storage.set('canvasHistory', this.conversationHistory.canvas); Utils.storage.set('canvasHistory', this.conversationHistory.canvas);
Utils.storage.set('swotHistory', this.conversationHistory.swot); Utils.storage.set('swotHistory', this.conversationHistory.swot);
Utils.storage.set('canvasSVGs', this.svgStorage.canvas); Utils.storage.set('canvasSVGs', this.svgStorage.canvas);
Utils.storage.set('swotSVGs', this.svgStorage.swot); Utils.storage.set('swotSVGs', this.svgStorage.swot);
Utils.scrollToBottom(this.chatHistory);
} }
// 清空当前对话 // 清空当前对话

View File

@@ -21,18 +21,52 @@ 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) { if (fenceMatch) {
const svgContent = match[1].trim(); const svgBody = fenceMatch[1].trim();
const beforeText = response.substring(0, match.index).trim(); const beforeText = content.substring(0, fenceMatch.index).trim();
const afterText = response.substring(match.index + match[0].length).trim(); let afterText = content.substring(fenceMatch.index + fenceMatch[0].length).trim();
afterText = afterText.replace(/^\s*```/, '').trim();
return { return {
svgContent, svgContent: svgBody,
beforeText,
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, beforeText,
afterText afterText
}; };
@@ -40,7 +74,7 @@ function parseSVGResponse(response) {
return { return {
svgContent: null, svgContent: null,
beforeText: response, beforeText: content.trim(),
afterText: '' afterText: ''
}; };
} }