- 在 js/utils.js:24-79 强化 parseSVGResponse,兼容缺失结尾反引号和截断的 SVG,自动补齐 </svg> 并去除残
留的 ```,确保后续渲染能稳定提取图形。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
// 流式发送聊天请求
|
||||
|
||||
268
js/app.js
268
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 = '<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>';
|
||||
};
|
||||
const onComplete = () => {
|
||||
// 流式接收完成,处理完整消息
|
||||
this.finalizeStreamingMessage(messageId, fullContent, svgId);
|
||||
|
||||
this.isProcessing = false;
|
||||
this.sendButton.disabled = false;
|
||||
this.sendButton.innerHTML = '<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>';
|
||||
};
|
||||
|
||||
// 调用流式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 = `
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
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('</svg>')) {
|
||||
const svgEndIndex = fullContent.indexOf('</svg>') + 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 = `
|
||||
<div>
|
||||
${parsedBeforeText}
|
||||
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
|
||||
📊 点击查看 ${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 {
|
||||
// 使用原有的解析方法作为后备
|
||||
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 = `
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存数据
|
||||
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 = `
|
||||
<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>
|
||||
`;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div>
|
||||
${beforeHtml}
|
||||
<div class="svg-placeholder-block" data-svg-id="${targetSvgId}" onclick="app.viewSVG('${targetSvgId}')">
|
||||
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
|
||||
</div>
|
||||
${afterHtml}
|
||||
</div>
|
||||
${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 = `
|
||||
<div class="mb-1">
|
||||
${parsedContent}
|
||||
</div>
|
||||
${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() {
|
||||
|
||||
82
js/utils.js
82
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*<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') {
|
||||
@@ -286,4 +320,4 @@ window.Utils = {
|
||||
autoResizeTextarea,
|
||||
StreamProcessor,
|
||||
createStreamRequest
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user