/** * 应用核心逻辑 */ // 配置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 = `

生成的${label}将在此处显示

`; 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*/i); if (svgStartMatch) { svgStarted = true; svgId = svgId || Utils.generateId('svg'); this.pendingSvgId = svgId; const svgStartIndex = svgStartMatch.index; beforeText = fullContent.substring(0, svgStartIndex); this.updateStreamingMessageWithPlaceholder(messageContainer, beforeText, svgId); this.setActivePlaceholder(svgId); this.svgViewer.innerHTML = `

正在绘制${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}...

`; this.updateZoomButtons(); } } if (svgStarted) { if (fullContent.includes('')) { const svgEndIndex = fullContent.indexOf('') + 6; const svgStartMatch = fullContent.match(/```(?:svg)?\s*/i); if (svgStartMatch) { const svgStartIndex = svgStartMatch.index; let svgWithMarkers = fullContent.substring(svgStartIndex, svgEndIndex); svgContent = svgWithMarkers.replace(/```(?:svg)?\s*/, '').replace(/```$/, '').trim(); if (!svgContent.endsWith('')) { 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*/i); if (svgStartMatch) { const svgStartIndex = svgStartMatch.index; let svgWithMarkers = fullContent.substring(svgStartIndex); svgContent = svgWithMarkers.replace(/```(?:svg)?\s*/, '').replace(/```$/, '').trim(); let tempSvgContent = svgContent; if (!tempSvgContent.endsWith('')) { 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('') ? parsed.svgContent.trim() : `${parsed.svgContent.trim()} `; 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('') ? parsed.svgContent.trim() : `${parsed.svgContent.trim()}\n`; 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 = ` `; 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(); });