- 在 js/utils.js:24-79 强化 parseSVGResponse,兼容缺失结尾反引号和截断的 SVG,自动补齐 </svg> 并去除残
留的 ```,确保后续渲染能稳定提取图形。
This commit is contained in:
@@ -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
268
js/app.js
@@ -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() {
|
||||||
|
|||||||
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)}`;
|
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
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user