/**
* 应用核心逻辑
*/
// 配置Markdown解析器
if (typeof marked !== 'undefined') {
marked.setOptions({
breaks: true, // 支持换行
gfm: true, // 支持GitHub风格的Markdown
sanitize: false, // 允许HTML(因为我们自己处理SVG)
smartLists: true, // 智能列表
smartypants: true // 智能标点
});
}
class ProductCanvasApp {
constructor() {
this.currentMode = 'canvas'; // 'canvas' 或 'swot'
this.svgStorage = {};
this.currentSvgId = null;
this.conversationHistory = {};
this.isProcessing = false;
this.activeStreamHandle = null;
this.svgZoom = { canvas: 1, swot: 1 };
this.activeSvgPlaceholder = null;
this.pendingSvgId = null;
this.pendingCancel = false;
this.copyClipboardSupported = typeof ClipboardItem !== 'undefined' && !!navigator.clipboard;
this.initElements();
this.initEventListeners();
this.loadSavedData();
this.updateModeUI();
this.setSendButtonState('idle');
}
getModeDisplayName(mode = this.currentMode) {
return mode === 'canvas' ? '产品画布' : 'SWOT分析';
}
// 初始化DOM元素引用
initElements() {
// 模式切换按钮
this.canvasBtn = document.getElementById('canvas-mode-btn');
this.swotBtn = document.getElementById('swot-mode-btn');
this.pageTitle = document.getElementById('page-title');
// 对话相关
this.chatInput = document.getElementById('chat-input');
this.sendButton = document.getElementById('send-button');
this.clearHistoryBtn = document.getElementById('clear-history-btn');
this.chatHistory = document.getElementById('chat-history');
// SVG显示
this.svgViewer = document.getElementById('svg-viewer');
this.placeholderText = this.svgViewer.querySelector('#placeholder-text');
// 底部操作按钮
this.zoomOutBtn = document.getElementById('zoom-out-btn');
this.zoomInBtn = document.getElementById('zoom-in-btn');
this.zoomResetBtn = document.getElementById('zoom-reset-btn');
this.downloadSvgBtn = document.getElementById('download-svg-btn');
this.copyImageBtn = document.getElementById('copy-image-btn');
this.exportImageBtn = document.getElementById('export-image-btn');
this.viewCodeBtn = document.getElementById('view-code-btn');
// API配置模态窗
this.settingsBtn = document.getElementById('settings-btn');
this.configModal = document.getElementById('config-modal');
this.closeModalBtn = document.getElementById('close-modal-btn');
this.apiUrlInput = document.getElementById('api-url');
this.apiKeyInput = document.getElementById('api-key');
this.apiModelInput = document.getElementById('api-model');
this.testApiBtn = document.getElementById('test-api-btn');
this.saveConfigBtn = document.getElementById('save-config-btn');
this.configStatus = document.getElementById('config-status');
this.statusText = document.getElementById('status-text');
}
// 初始化事件监听器
initEventListeners() {
// 模式切换
this.canvasBtn.addEventListener('click', () => this.switchMode('canvas'));
this.swotBtn.addEventListener('click', () => this.switchMode('swot'));
// 发送消息
this.sendButton.addEventListener('click', () => {
if (this.isProcessing) {
this.cancelActiveStream();
} else {
this.sendMessage();
}
});
this.clearHistoryBtn.addEventListener('click', () => this.clearCurrentConversation());
// 输入框事件
this.chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!this.isProcessing) {
this.sendMessage();
}
}
});
// 自动调整输入框高度
this.chatInput.addEventListener('input', () => {
Utils.autoResizeTextarea(this.chatInput);
});
// 底部操作按钮
if (this.zoomOutBtn) this.zoomOutBtn.addEventListener('click', () => this.adjustSvgZoom(-0.25));
if (this.zoomInBtn) this.zoomInBtn.addEventListener('click', () => this.adjustSvgZoom(0.25));
if (this.zoomResetBtn) this.zoomResetBtn.addEventListener('click', () => this.resetSvgZoom());
this.downloadSvgBtn.addEventListener('click', () => this.downloadSVG());
if (this.copyImageBtn) {
if (this.copyClipboardSupported) {
this.copyImageBtn.addEventListener('click', () => this.copySvgToClipboard());
} else {
this.copyImageBtn.disabled = true;
this.copyImageBtn.title = '当前浏览器不支持复制图片到剪贴板';
}
}
this.exportImageBtn.addEventListener('click', () => this.exportAsImage());
this.viewCodeBtn.addEventListener('click', () => this.viewSVGCode());
// API配置模态窗
this.settingsBtn.addEventListener('click', () => this.openConfigModal());
this.closeModalBtn.addEventListener('click', () => this.closeConfigModal());
this.configModal.addEventListener('click', (e) => {
if (e.target === this.configModal) {
this.closeConfigModal();
}
});
this.testApiBtn.addEventListener('click', () => this.testAPIConnection());
this.saveConfigBtn.addEventListener('click', () => this.saveAPIConfig());
}
// 加载保存的数据
loadSavedData() {
// 加载模式
const savedMode = Utils.storage.get('currentMode', 'canvas');
this.currentMode = savedMode;
// 加载对话历史(按模式分别存储)
const savedCanvasHistory = Utils.storage.get('canvasHistory', []);
const savedSwotHistory = Utils.storage.get('swotHistory', []);
this.conversationHistory = {
canvas: savedCanvasHistory,
swot: savedSwotHistory
};
this.renderConversationHistory();
// 加载SVG存储(按模式分别存储)
const savedCanvasSVGs = Utils.storage.get('canvasSVGs', {});
const savedSwotSVGs = Utils.storage.get('swotSVGs', {});
this.svgStorage = {
canvas: savedCanvasSVGs,
swot: savedSwotSVGs
};
this.renderSvgViewerForMode();
this.setSendButtonState('idle');
// 加载API配置
const apiConfig = window.apiClient.getConfig();
this.apiUrlInput.value = apiConfig.url || '';
this.apiKeyInput.value = apiConfig.key || '';
this.apiModelInput.value = apiConfig.model || '';
}
// 切换模式
switchMode(mode) {
if (this.currentMode === mode) return;
this.currentMode = mode;
Utils.storage.set('currentMode', mode);
this.currentSvgId = null;
this.showSvgPlaceholder();
this.updateModeUI();
this.renderConversationHistory();
this.renderSvgViewerForMode();
}
// 更新模式UI
updateModeUI() {
const isCanvas = this.currentMode === 'canvas';
if (isCanvas) {
this.canvasBtn.classList.add('mode-btn-active');
this.canvasBtn.classList.remove('mode-btn-inactive');
this.swotBtn.classList.remove('mode-btn-active');
this.swotBtn.classList.add('mode-btn-inactive');
} else {
this.swotBtn.classList.add('mode-btn-active');
this.swotBtn.classList.remove('mode-btn-inactive');
this.canvasBtn.classList.remove('mode-btn-active');
this.canvasBtn.classList.add('mode-btn-inactive');
}
this.pageTitle.textContent = isCanvas ? '产品画布' : 'SWOT分析';
if (!this.currentSvgId) {
const placeholder = this.svgViewer.querySelector('#placeholder-text');
if (placeholder) {
placeholder.textContent = `生成的${this.getModeDisplayName()}将在此处显示`;
}
}
}
setSendButtonState(state) {
this.sendButtonState = state;
if (!this.sendButton) return;
if (state === 'streaming') {
this.sendButton.innerHTML = `
终止
`;
this.sendButton.classList.add('terminate-mode');
this.sendButton.title = '终止当前生成';
} else if (state === 'terminating') {
this.sendButton.innerHTML = `
终止中
`;
this.sendButton.classList.add('terminate-mode');
this.sendButton.title = '正在终止生成';
} else if (state === 'busy') {
this.sendButton.innerHTML = `
处理中
`;
this.sendButton.classList.add('terminate-mode');
this.sendButton.title = '正在处理请求';
} else {
this.sendButton.innerHTML = '';
this.sendButton.classList.remove('terminate-mode');
this.sendButton.title = '发送';
}
}
showSvgPlaceholder() {
const label = this.getModeDisplayName();
this.currentSvgId = null;
this.svgViewer.innerHTML = `
`;
this.placeholderText = this.svgViewer.querySelector('#placeholder-text');
this.setActivePlaceholder(null);
this.updateZoomButtons();
}
renderSvgViewerForMode() {
const svgStore = this.svgStorage[this.currentMode] || {};
const history = this.conversationHistory[this.currentMode] || [];
let latestSvgId = null;
for (let i = history.length - 1; i >= 0; i--) {
const message = history[i];
if (message.type !== 'ai') continue;
for (const [svgId, svg] of Object.entries(svgStore)) {
if (svg.messageId === message.id) {
latestSvgId = svgId;
break;
}
}
if (latestSvgId) break;
}
if (latestSvgId && svgStore[latestSvgId]) {
this.currentSvgId = latestSvgId;
this.mountSvgMarkup(svgStore[latestSvgId].content);
this.setActivePlaceholder(latestSvgId);
} else {
this.currentSvgId = null;
this.showSvgPlaceholder();
}
this.updateZoomButtons();
}
adjustSvgZoom(delta) {
const current = this.svgZoom[this.currentMode] || 1;
const next = Math.min(3, Math.max(0.25, parseFloat((current + delta).toFixed(2))));
this.svgZoom[this.currentMode] = next;
this.applySvgZoom();
}
resetSvgZoom() {
this.svgZoom[this.currentMode] = 1;
this.applySvgZoom();
}
applySvgZoom() {
const zoom = this.svgZoom[this.currentMode] || 1;
const wrapper = this.svgViewer.querySelector('.svg-content-wrapper');
if (wrapper) {
wrapper.style.transform = `scale(${zoom})`;
wrapper.style.transformOrigin = 'center top';
}
this.updateZoomButtons();
}
updateZoomButtons() {
if (!this.zoomInBtn || !this.zoomOutBtn || !this.zoomResetBtn) return;
const hasActiveSvg = !!this.currentSvgId && !!(this.svgStorage[this.currentMode] || {})[this.currentSvgId];
const zoom = this.svgZoom[this.currentMode] || 1;
const disableControls = !hasActiveSvg;
this.zoomInBtn.disabled = disableControls || zoom >= 3;
this.zoomOutBtn.disabled = disableControls || zoom <= 0.25;
this.zoomResetBtn.disabled = disableControls || Math.abs(zoom - 1) < 0.01;
if (!hasActiveSvg) {
if (this.copyImageBtn) this.copyImageBtn.disabled = true;
this.downloadSvgBtn.disabled = true;
this.exportImageBtn.disabled = true;
this.viewCodeBtn.disabled = true;
} else {
if (this.copyImageBtn) this.copyImageBtn.disabled = !this.copyClipboardSupported;
this.downloadSvgBtn.disabled = false;
this.exportImageBtn.disabled = false;
this.viewCodeBtn.disabled = false;
}
}
setActivePlaceholder(svgId) {
const previousActive = this.chatHistory.querySelectorAll('.svg-placeholder-active');
previousActive.forEach(el => el.classList.remove('svg-placeholder-active'));
if (svgId) {
const next = this.chatHistory.querySelector(`.svg-placeholder-block[data-svg-id="${svgId}"], .svg-drawing-placeholder[data-svg-id="${svgId}"]`);
if (next) {
next.classList.add('svg-placeholder-active');
this.activeSvgPlaceholder = svgId;
} else {
this.activeSvgPlaceholder = null;
}
} else {
this.activeSvgPlaceholder = null;
}
}
mountSvgMarkup(svgMarkup, temporary = false) {
this.svgViewer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.className = 'svg-content-wrapper';
wrapper.innerHTML = svgMarkup;
this.svgViewer.appendChild(wrapper);
this.placeholderText = null;
if (!temporary) {
this.applySvgZoom();
} else {
const zoom = this.svgZoom[this.currentMode] || 1;
wrapper.style.transform = `scale(${zoom})`;
wrapper.style.transformOrigin = 'center top';
}
}
renderSvgContent(svgId) {
const store = this.svgStorage[this.currentMode] || {};
if (!svgId || !store[svgId]) {
this.showSvgPlaceholder();
return;
}
this.currentSvgId = svgId;
this.mountSvgMarkup(store[svgId].content);
this.setActivePlaceholder(svgId);
this.applySvgZoom();
}
renderTemporarySvg(svgMarkup) {
this.mountSvgMarkup(svgMarkup, true);
}
buildContextForUserMessage(userIndex) {
const history = this.conversationHistory[this.currentMode] || [];
if (userIndex < 0 || userIndex >= history.length) {
return null;
}
const target = history[userIndex];
if (!target || target.type !== 'user') {
return null;
}
const start = Math.max(0, userIndex - 10);
const contextSlice = history.slice(start, userIndex);
const contextMessages = contextSlice
.filter(msg => msg.type === 'user' || msg.type === 'ai')
.map(msg => ({
role: msg.type === 'user' ? 'user' : 'assistant',
content: msg.content
}));
return {
userMessage: target.content,
contextMessages
};
}
// 发送消息
async sendMessage() {
const message = this.chatInput.value.trim();
if (!message || this.isProcessing) return;
if (!window.apiClient.isConfigValid()) {
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
this.openConfigModal();
return;
}
this.pendingCancel = false;
this.isProcessing = true;
this.addUserMessage(message);
this.chatInput.value = '';
Utils.autoResizeTextarea(this.chatInput);
const history = this.conversationHistory[this.currentMode] || [];
const targetIndex = history.length - 1;
const payload = this.buildContextForUserMessage(targetIndex);
if (!payload) {
console.warn('无法构建上下文,终止发送');
this.isProcessing = false;
this.setSendButtonState('idle');
return;
}
this.setSendButtonState('streaming');
this.sendButton.disabled = false;
try {
await this.startStreamingMessage(payload.userMessage, payload.contextMessages);
} catch (error) {
console.error('发送消息失败:', error);
this.addErrorMessage(error.message);
this.isProcessing = false;
this.setSendButtonState('idle');
this.activeStreamHandle = null;
this.sendButton.disabled = false;
}
}
// 开始流式接收消息
async startStreamingMessage(userMessage, contextMessages) {
const messageId = Utils.generateId('msg');
const messageContainer = this.createStreamingMessageContainer(messageId);
this.chatHistory.appendChild(messageContainer);
Utils.scrollToBottom(this.chatHistory);
let fullContent = '';
let svgStarted = false;
let svgContent = '';
let svgId = null;
let beforeText = '';
let streamClosed = false;
this.activeStreamHandle = null;
const finalizeStream = (info = {}) => {
if (streamClosed) return;
streamClosed = true;
const trimmedContent = fullContent.trim();
if (!trimmedContent && !svgId) {
const bubble = this.chatHistory.querySelector(`[data-message-id="${messageId}"]`);
if (bubble) {
const wrapper = bubble.closest('.flex');
if (wrapper) wrapper.remove();
}
} else {
this.finalizeStreamingMessage(messageId, fullContent, svgId);
}
this.isProcessing = false;
this.setSendButtonState('idle');
this.sendButton.disabled = false;
this.activeStreamHandle = null;
this.pendingCancel = false;
};
const onChunk = (chunk) => {
if (
streamClosed ||
!chunk ||
!chunk.choices ||
!chunk.choices[0] ||
!chunk.choices[0].delta
) {
return;
}
const content = chunk.choices[0].delta.content || '';
if (!content) return;
fullContent += content;
if (!svgStarted) {
const svgStartMatch = fullContent.match(/```(?:svg)?\s*')) {
const svgEndIndex = fullContent.indexOf('') + 6;
const svgStartMatch = fullContent.match(/```(?:svg)?\s*')) {
svgContent += '';
}
this.svgStorage[this.currentMode][svgId] = {
content: svgContent,
messageId,
mode: this.currentMode,
timestamp: new Date().toISOString()
};
this.pendingSvgId = null;
this.renderSvgContent(svgId);
this.updatePlaceholderToClickable(messageContainer, svgId);
svgStarted = false;
const afterText = fullContent.substring(svgEndIndex);
this.updateStreamingMessageAfterSVG(messageContainer, beforeText, svgId, afterText);
}
} else {
const svgStartMatch = fullContent.match(/```(?:svg)?\s*')) {
tempSvgContent += '';
}
this.renderTemporarySvg(tempSvgContent);
}
}
} else {
this.updateStreamingMessage(messageContainer, fullContent);
}
};
const onComplete = (info = {}) => {
finalizeStream(info);
};
try {
const streamHandle = await (
this.currentMode === 'canvas'
? window.apiClient.generateProductCanvasStream(userMessage, contextMessages, onChunk, onComplete)
: window.apiClient.generateSWOTAnalysisStream(userMessage, contextMessages, onChunk, onComplete)
);
this.activeStreamHandle = streamHandle;
if (this.pendingCancel) {
const shouldCancel = this.pendingCancel;
this.pendingCancel = false;
this.cancelActiveStream();
}
await streamHandle.finished;
if (!streamClosed) {
finalizeStream({ aborted: false });
}
} catch (error) {
streamClosed = true;
this.activeStreamHandle = null;
this.isProcessing = false;
this.setSendButtonState('idle');
this.sendButton.disabled = false;
this.pendingCancel = false;
const bubble = this.chatHistory.querySelector(`[data-message-id="${messageId}"]`);
if (bubble) {
const wrapper = bubble.closest('.flex');
if (wrapper) wrapper.remove();
}
if (error && error.name === 'AbortError') {
return;
}
console.error('发送消息失败:', error);
this.addErrorMessage(error.message || '生成失败');
}
}
cancelActiveStream() {
if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') {
this.pendingCancel = true;
this.setSendButtonState('terminating');
return;
}
this.pendingCancel = false;
this.setSendButtonState('terminating');
try {
this.activeStreamHandle.cancel();
} catch (error) {
console.warn('终止流式请求失败:', error);
}
}
// 创建流式消息容器
createStreamingMessageContainer(messageId) {
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-start';
messageDiv.dataset.messageId = messageId;
messageDiv.innerHTML = `
`;
return messageDiv;
}
// 更新流式消息内容
updateStreamingMessage(container, content) {
const contentDiv = container.querySelector('.typing-cursor');
if (contentDiv) {
// 使用Markdown解析内容
if (typeof marked !== 'undefined') {
contentDiv.innerHTML = marked.parse(content);
} else {
contentDiv.textContent = content;
}
Utils.scrollToBottom(this.chatHistory);
}
}
// 更新流式消息内容并显示SVG占位符
updateStreamingMessageWithPlaceholder(container, beforeText, svgId) {
// 使用Markdown解析beforeText
const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText);
container.innerHTML = `
${parsedBeforeText}
🎨 正在绘制${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}...
`;
Utils.scrollToBottom(this.chatHistory);
}
// 更新占位符为可点击状态
updatePlaceholderToClickable(container, svgId) {
const placeholder = container.querySelector('.svg-drawing-placeholder');
if (placeholder) {
placeholder.classList.remove('svg-drawing-placeholder');
placeholder.classList.add('svg-placeholder-block');
placeholder.setAttribute('data-svg-id', svgId);
placeholder.innerHTML = `📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG`;
placeholder.setAttribute('onclick', `app.viewSVG('${svgId}')`);
if (this.currentSvgId === svgId) {
placeholder.classList.add('svg-placeholder-active');
}
}
}
// 更新SVG后的消息内容
updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) {
const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText);
const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText);
const placeholderClass = this.currentSvgId === svgId ? 'svg-placeholder-block svg-placeholder-active' : 'svg-placeholder-block';
container.innerHTML = `
${parsedBeforeText}
📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
${parsedAfterText}
`;
Utils.scrollToBottom(this.chatHistory);
}
buildActionToolbar(messageId, { allowRegenerate = false, allowRollback = true } = {}) {
const actions = [];
if (allowRollback) {
actions.push(`
`);
}
if (allowRegenerate) {
actions.push(`
`);
}
if (!actions.length) {
return '';
}
return `
${actions.join('')}
`;
}
// 组装标准化的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) {
this.pendingSvgId = null;
const parsed = Utils.parseSVGResponse(fullContent);
const timestamp = new Date().toISOString();
const message = {
id: messageId,
type: 'ai',
content: '',
timestamp
};
let targetSvgId = svgId || null;
if (parsed.svgContent && parsed.svgContent.includes('`;
if (!targetSvgId || !this.svgStorage[this.currentMode][targetSvgId]) {
targetSvgId = targetSvgId || Utils.generateId('svg');
}
this.svgStorage[this.currentMode][targetSvgId] = {
content: svgBody,
messageId,
mode: this.currentMode,
timestamp
};
this.currentSvgId = targetSvgId;
this.svgViewer.innerHTML = svgBody;
message.content = this.buildSVGMessageContent(parsed.beforeText, svgBody, parsed.afterText);
} else {
const sanitizedText = fullContent.replace(/^[\s`]+/, '').replace(/[\s`]+$/, '').trim();
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);
this.renderConversationHistory();
this.renderSvgViewerForMode();
Utils.scrollToBottom(this.chatHistory);
}
// 清空当前对话
clearCurrentConversation() {
if (!confirm(`确定要清空当前的${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}对话吗?`)) {
return;
}
// 清空当前模式的对话历史
this.conversationHistory[this.currentMode] = [];
// 清空当前模式的SVG存储
this.svgStorage[this.currentMode] = {};
this.svgZoom[this.currentMode] = 1;
this.showSvgPlaceholder();
// 保存数据
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);
// 重新渲染对话历史
this.renderConversationHistory();
this.renderSvgViewerForMode();
}
// 添加用户消息
addUserMessage(text) {
const messageId = Utils.generateId('msg');
const message = {
id: messageId,
type: 'user',
content: text,
timestamp: new Date().toISOString()
};
this.conversationHistory[this.currentMode].push(message);
this.renderMessage(message);
Utils.scrollToBottom(this.chatHistory);
Utils.storage.set('canvasHistory', this.conversationHistory.canvas);
Utils.storage.set('swotHistory', this.conversationHistory.swot);
}
// 添加AI消息(非流式,保留用于错误情况)
addAIMessage(text) {
const messageId = Utils.generateId('msg');
const parsed = Utils.parseSVGResponse(text);
const message = {
id: messageId,
type: 'ai',
content: text,
timestamp: new Date().toISOString()
};
this.conversationHistory[this.currentMode].push(message);
let svgId = null;
if (parsed.svgContent) {
svgId = Utils.generateId('svg');
this.svgStorage[this.currentMode][svgId] = {
content: parsed.svgContent,
messageId,
mode: this.currentMode,
timestamp: new Date().toISOString()
};
this.viewSVG(svgId);
}
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);
this.renderConversationHistory();
this.renderSvgViewerForMode();
}
// 添加错误消息
addErrorMessage(errorText) {
const messageId = Utils.generateId('msg');
const message = {
id: messageId,
type: 'error',
content: errorText,
timestamp: new Date().toISOString()
};
this.conversationHistory[this.currentMode].push(message);
this.renderMessage(message);
Utils.scrollToBottom(this.chatHistory);
Utils.storage.set('canvasHistory', this.conversationHistory.canvas);
Utils.storage.set('swotHistory', this.conversationHistory.swot);
}
// 渲染消息
renderMessage(message, options = {}) {
const { allowRegenerate = false, allowRollback = message.type === 'ai' } = options;
const messageDiv = document.createElement('div');
if (message.type === 'user') {
messageDiv.className = 'flex justify-end';
messageDiv.innerHTML = `
${Utils.escapeHtml(message.content)}
`;
} else if (message.type === 'error') {
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
${Utils.escapeHtml(message.content)}
`;
} else if (message.type === 'ai') {
const parsedContent = typeof marked !== 'undefined' ? marked.parse(message.content) : Utils.escapeHtml(message.content);
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
${parsedContent}
${actions}
`;
}
this.chatHistory.appendChild(messageDiv);
}
// 渲染包含SVG的消息
renderMessageWithSVG(message, parsed, svgId, options = {}) {
const { allowRegenerate = false, allowRollback = true } = options;
const messageDiv = document.createElement('div');
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)) : '';
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
messageDiv.className = 'flex justify-start';
const placeholderClass = this.currentSvgId === svgId ? 'svg-placeholder-block svg-placeholder-active' : 'svg-placeholder-block';
messageDiv.innerHTML = `
${beforeHtml}
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
${afterHtml}
${actions}
`;
this.chatHistory.appendChild(messageDiv);
}
// 渲染对话历史
renderConversationHistory() {
this.chatHistory.innerHTML = '';
// 获取当前模式的对话历史
const currentHistory = this.conversationHistory[this.currentMode] || [];
const currentSvgStorage = this.svgStorage[this.currentMode] || {};
let hasStorageUpdate = false;
let hasHistoryUpdate = false;
let lastAiMessageId = null;
for (let i = currentHistory.length - 1; i >= 0; i--) {
if (currentHistory[i].type === 'ai') {
lastAiMessageId = currentHistory[i].id;
break;
}
}
for (const message of currentHistory) {
if (message.type === 'ai') {
const parsed = Utils.parseSVGResponse(message.content);
// 查找或补建对应的SVG
let svgId = null;
for (const [id, svg] of Object.entries(currentSvgStorage)) {
if (svg.messageId === message.id) {
svgId = id;
break;
}
}
const hasSvgContent = parsed.svgContent && parsed.svgContent.includes('`;
svgId = Utils.generateId('svg');
currentSvgStorage[svgId] = {
content: normalizedSvg,
messageId: message.id,
mode: this.currentMode,
timestamp: message.timestamp || new Date().toISOString()
};
parsed.svgContent = normalizedSvg;
message.content = this.buildSVGMessageContent(parsed.beforeText, normalizedSvg, parsed.afterText);
hasStorageUpdate = true;
hasHistoryUpdate = true;
}
this.renderMessageWithSVG(message, parsed, svgId, { allowRegenerate: message.id === lastAiMessageId });
continue;
}
this.renderMessage(message, { allowRegenerate: message.id === lastAiMessageId });
} else {
this.renderMessage(message);
}
}
if (hasStorageUpdate) {
this.svgStorage[this.currentMode] = currentSvgStorage;
Utils.storage.set('canvasSVGs', this.svgStorage.canvas || {});
Utils.storage.set('swotSVGs', this.svgStorage.swot || {});
}
if (hasHistoryUpdate) {
Utils.storage.set('canvasHistory', this.conversationHistory.canvas || []);
Utils.storage.set('swotHistory', this.conversationHistory.swot || []);
}
this.setActivePlaceholder(this.currentSvgId);
Utils.scrollToBottom(this.chatHistory);
}
// 显示SVG
viewSVG(svgId) {
const store = this.svgStorage[this.currentMode] || {};
if (!store[svgId]) {
console.error('SVG not found:', svgId);
return;
}
this.renderSvgContent(svgId);
}
// 退回到指定消息
rollbackToMessage(messageId) {
const messageIndex = this.conversationHistory[this.currentMode].findIndex(msg => msg.id === messageId);
if (messageIndex === -1) return;
// 删除指定消息之后的所有消息
const messagesToRemove = this.conversationHistory[this.currentMode].slice(messageIndex + 1);
// 删除相关的SVG
for (const message of messagesToRemove) {
for (const [svgId, svg] of Object.entries(this.svgStorage[this.currentMode])) {
if (svg.messageId === message.id) {
delete this.svgStorage[this.currentMode][svgId];
// 如果当前显示的是被删除的SVG,清空显示
if (this.currentSvgId === svgId) {
this.showSvgPlaceholder();
}
}
}
}
// 更新对话历史
this.conversationHistory[this.currentMode] = this.conversationHistory[this.currentMode].slice(0, messageIndex + 1);
// 保存数据
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);
// 重新渲染对话历史
this.renderConversationHistory();
this.renderSvgViewerForMode();
}
// 重新生成消息
async regenerateMessage(messageId) {
if (this.isProcessing) return;
const history = this.conversationHistory[this.currentMode] || [];
const targetIndex = history.findIndex(msg => msg.id === messageId && msg.type === 'ai');
if (targetIndex === -1) {
console.warn('未找到可重新生成的消息');
return;
}
let userIndex = -1;
for (let i = targetIndex - 1; i >= 0; i--) {
if (history[i].type === 'user') {
userIndex = i;
break;
}
}
if (userIndex === -1) {
alert('未找到对应的用户消息,无法重新生成');
return;
}
const payload = this.buildContextForUserMessage(userIndex);
if (!payload) {
alert('无法构建对话上下文,请稍后重试');
return;
}
this.pendingCancel = false;
this.isProcessing = true;
this.setSendButtonState('streaming');
this.sendButton.disabled = false;
try {
await this.startStreamingMessage(payload.userMessage, payload.contextMessages);
} catch (error) {
console.error('重新生成失败:', error);
this.addErrorMessage(error.message);
this.isProcessing = false;
this.setSendButtonState('idle');
this.activeStreamHandle = null;
this.sendButton.disabled = false;
}
}
getActiveSvgRecord(showWarning = true) {
const store = this.svgStorage[this.currentMode] || {};
if (this.currentSvgId && store[this.currentSvgId]) {
return { id: this.currentSvgId, ...store[this.currentSvgId] };
}
if (showWarning) {
alert('请先生成并选择一个图表');
}
return null;
}
parseSvgDimensions(svgContent) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(svgContent, 'image/svg+xml');
const svgEl = doc.querySelector('svg');
if (!svgEl) {
return { width: 1024, height: 768 };
}
const parseLength = (value) => {
if (!value) return null;
const match = /([0-9.]+)/.exec(value);
return match ? parseFloat(match[1]) : null;
};
let width = parseLength(svgEl.getAttribute('width'));
let height = parseLength(svgEl.getAttribute('height'));
const viewBox = svgEl.getAttribute('viewBox');
if ((!width || !height) && viewBox) {
const parts = viewBox.split(/\s+/).map(Number).filter(n => !Number.isNaN(n));
if (parts.length === 4) {
width = width || parts[2];
height = height || parts[3];
}
}
return {
width: width || 1024,
height: height || 768
};
} catch (error) {
console.warn('解析SVG尺寸失败:', error);
return { width: 1024, height: 768 };
}
}
loadImageFromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = url;
});
}
async convertSvgToPngBlob(svgContent) {
const { width, height } = this.parseSvgDimensions(svgContent);
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(svgBlob);
try {
const img = await this.loadImageFromUrl(url);
const canvas = document.createElement('canvas');
const canvasWidth = Math.max(1, img.naturalWidth || width || 1024);
const canvasHeight = Math.max(1, img.naturalHeight || height || 768);
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
return await new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('无法生成PNG图像'));
}
}, 'image/png');
});
} finally {
URL.revokeObjectURL(url);
}
}
async copySvgToClipboard() {
const record = this.getActiveSvgRecord();
if (!record) return;
if (!navigator.clipboard || typeof ClipboardItem === 'undefined') {
alert('当前浏览器不支持复制图片到剪贴板,请使用下载功能。');
return;
}
try {
const blob = await this.convertSvgToPngBlob(record.content);
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
alert('图像已复制到剪贴板');
} catch (error) {
console.error('复制图片失败:', error);
alert('复制失败,请稍后重试。');
}
}
// 下载SVG
downloadSVG() {
const record = this.getActiveSvgRecord();
if (!record) return;
const filename = `${this.currentMode}-${Utils.formatDateTime().replace(/[/:]/g, '-')}.svg`;
Utils.downloadFile(record.content, filename, 'image/svg+xml');
}
// 导出为图片
async exportAsImage() {
const record = this.getActiveSvgRecord();
if (!record) return;
try {
const blob = await this.convertSvgToPngBlob(record.content);
const filename = `${this.currentMode}-${Utils.formatDateTime().replace(/[/:]/g, '-')}.png`;
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error('导出图片失败:', error);
alert('导出图片失败,请稍后重试。');
}
}
// 查看SVG代码
viewSVGCode() {
const record = this.getActiveSvgRecord();
if (!record) return;
const svgContent = record.content;
// 创建代码查看模态窗
const modal = document.createElement('div');
modal.className = 'modal-overlay active';
modal.innerHTML = `
${Utils.escapeHtml(svgContent)}
`;
document.body.appendChild(modal);
// 关闭模态窗
const closeModal = () => {
document.body.removeChild(modal);
};
modal.querySelector('.close-modal').addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
});
// 复制代码
modal.querySelector('.copy-btn').addEventListener('click', () => {
navigator.clipboard.writeText(svgContent).then(() => {
const btn = modal.querySelector('.copy-btn');
const originalHTML = btn.innerHTML;
btn.innerHTML = ' 已复制';
btn.classList.remove('bg-blue-500', 'hover:bg-blue-600');
btn.classList.add('bg-green-500', 'hover:bg-green-600');
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove('bg-green-500', 'hover:bg-green-600');
btn.classList.add('bg-blue-500', 'hover:bg-blue-600');
}, 2000);
});
});
}
// 打开API配置模态窗
openConfigModal() {
this.configModal.classList.add('active');
const apiConfig = window.apiClient.getConfig();
this.apiUrlInput.value = apiConfig.url || '';
this.apiKeyInput.value = apiConfig.key || '';
this.apiModelInput.value = apiConfig.model || '';
}
// 关闭API配置模态窗
closeConfigModal() {
this.configModal.classList.remove('active');
}
// 保存API配置
saveAPIConfig() {
const config = {
url: this.apiUrlInput.value.trim(),
key: this.apiKeyInput.value.trim(),
model: this.apiModelInput.value.trim()
};
if (!config.url || !config.key || !config.model) {
Utils.showStatus(this.configStatus, '⚠️ 请填写所有字段', 'error');
return;
}
window.apiClient.saveConfig(config);
Utils.showStatus(this.configStatus, '✅ 配置已保存成功!', 'success');
setTimeout(() => {
this.closeConfigModal();
}, 1500);
}
// 测试API连接
async testAPIConnection() {
const config = {
url: this.apiUrlInput.value.trim(),
key: this.apiKeyInput.value.trim(),
model: this.apiModelInput.value.trim()
};
if (!config.url || !config.key || !config.model) {
Utils.showStatus(this.configStatus, '⚠️ 请先填写所有字段', 'error');
return;
}
Utils.showStatus(this.configStatus, '🔄 正在测试连接...', 'loading');
try {
// 临时保存配置进行测试
window.apiClient.saveConfig(config);
await window.apiClient.testConnection();
Utils.showStatus(this.configStatus, '✅ 连接测试成功!', 'success');
} catch (error) {
Utils.showStatus(this.configStatus, `❌ 连接失败: ${error.message}`, 'error');
}
}
}
// 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => {
window.app = new ProductCanvasApp();
});