From 9032b634e675a2230f94a1e27165071efa99e4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B2=E6=82=A6?= Date: Fri, 24 Oct 2025 18:12:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/style.css | 64 +++++++++++ index.html | 24 ++-- js/app.js | 299 ++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 320 insertions(+), 67 deletions(-) diff --git a/css/style.css b/css/style.css index 98690e4..5cce7a8 100644 --- a/css/style.css +++ b/css/style.css @@ -33,6 +33,8 @@ body { color: #1f2937; padding: 10px 14px; max-width: 85%; + max-height: 300px; + overflow-y: auto; border: 2px solid #10b981; box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3); } @@ -59,6 +61,57 @@ body { background: linear-gradient(135deg, #fb923c 0%, #f87171 100%); } +/* SVG绘制中状态占位符 */ +.svg-drawing-placeholder { + display: block; + background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); + color: white; + padding: 8px 14px; + margin: 8px 0; + border: 2px solid #000; + box-shadow: 3px 3px 0 rgba(0,0,0,0.25); + font-weight: bold; + font-size: 13px; + cursor: pointer; + transition: all 0.2s; + text-align: center; + position: relative; + overflow: hidden; +} + +.svg-drawing-placeholder:hover { + transform: translateX(2px) translateY(-2px); + box-shadow: 4px 4px 0 rgba(0,0,0,0.3); + background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%); +} + +/* 绘制中动画效果 */ +@keyframes drawing-pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + +.svg-drawing-placeholder::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: drawing-shine 2s infinite; +} + +@keyframes drawing-shine { + 0% { left: -100%; } + 100% { left: 100%; } +} + +.svg-drawing-text { + animation: drawing-pulse 1.5s infinite; +} + /* 气泡操作按钮 */ .bubble-action-btn { opacity: 0; @@ -179,4 +232,15 @@ body { animation: blink 1s infinite; color: #667eea; font-weight: bold; +} + +/* 清空历史按钮摇动动画 */ +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); } + 20%, 40%, 60%, 80% { transform: translateX(2px); } +} + +.clear-history-btn:hover iconify-icon { + animation: shake 0.5s ease-in-out; } \ No newline at end of file diff --git a/index.html b/index.html index db6ba2a..1859cda 100644 --- a/index.html +++ b/index.html @@ -46,6 +46,17 @@
+ +
+
+ + 对话历史 +
+ +
@@ -58,21 +69,16 @@
-
+
-
- - -
+
diff --git a/js/app.js b/js/app.js index fed973c..4bd8e40 100644 --- a/js/app.js +++ b/js/app.js @@ -27,7 +27,7 @@ class ProductCanvasApp { // 对话相关 this.chatInput = document.getElementById('chat-input'); this.sendButton = document.getElementById('send-button'); - this.clearButton = document.getElementById('clear-button'); + this.clearHistoryBtn = document.getElementById('clear-history-btn'); this.chatHistory = document.getElementById('chat-history'); // SVG显示 @@ -60,7 +60,7 @@ class ProductCanvasApp { // 发送消息 this.sendButton.addEventListener('click', () => this.sendMessage()); - this.clearButton.addEventListener('click', () => this.clearCurrentConversation()); + this.clearHistoryBtn.addEventListener('click', () => this.clearCurrentConversation()); // 输入框事件 this.chatInput.addEventListener('keypress', (e) => { @@ -206,18 +206,111 @@ class ProductCanvasApp { Utils.scrollToBottom(this.chatHistory); let fullContent = ''; + let svgStarted = false; + let svgContent = ''; + let svgId = null; + let beforeText = ''; const onChunk = (chunk) => { if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) { const content = chunk.choices[0].delta.content || ''; fullContent += content; - this.updateStreamingMessage(messageContainer, fullContent); + + // 检测SVG开始标记 + if (!svgStarted && (fullContent.includes('```svg') || fullContent.includes('``` +
+ +

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

+
+
+ `; + } + + // 如果SVG已经开始,收集SVG内容 + if (svgStarted) { + // 检查是否有SVG结束标记 + if (fullContent.includes('')) { + const svgEndIndex = fullContent.indexOf('') + 6; // +6 是 '' 的长度 + + // 提取完整的SVG内容 + const svgStartIndex = Math.max( + fullContent.indexOf('```svg'), + fullContent.indexOf('``` ')) { + svgContent += ''; + } + + // 实时显示SVG + this.svgViewer.innerHTML = svgContent; + + // 存储SVG内容 + this.svgStorage[this.currentMode][svgId] = { + content: svgContent, + messageId: messageId, + mode: this.currentMode, + timestamp: new Date().toISOString() + }; + + // 更新占位符为可点击状态 + this.updatePlaceholderToClickable(messageContainer, svgId); + + // 重置SVG状态,继续接收剩余文本 + svgStarted = false; + const afterText = fullContent.substring(svgEndIndex); + this.updateStreamingMessageAfterSVG(messageContainer, beforeText, svgId, afterText); + } else if (svgContent) { + // SVG还在继续,更新内容 + const svgStartIndex = Math.max( + fullContent.indexOf('```svg'), + fullContent.indexOf('``` ')) { + tempSvgContent += ''; + } + + // 实时更新SVG显示 + this.svgViewer.innerHTML = tempSvgContent; + } + } else { + // 普通文本更新 + this.updateStreamingMessage(messageContainer, fullContent); + } } }; const onComplete = () => { // 流式接收完成,处理完整消息 - this.finalizeStreamingMessage(messageId, fullContent); + this.finalizeStreamingMessage(messageId, fullContent, svgId, beforeText); this.isProcessing = false; this.sendButton.disabled = false; @@ -252,14 +345,55 @@ class ProductCanvasApp { Utils.scrollToBottom(this.chatHistory); } } + + // 更新流式消息内容并显示SVG占位符 + updateStreamingMessageWithPlaceholder(container, beforeText, svgId) { + container.innerHTML = ` +
+
+ ${Utils.escapeHtml(beforeText)} +
+ 🎨 正在绘制${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.innerHTML = `📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG`; + placeholder.setAttribute('onclick', `app.viewSVG('${svgId}')`); + } + } + + // 更新SVG后的消息内容 + updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) { + container.innerHTML = ` +
+
+ ${Utils.escapeHtml(beforeText)} +
+ 📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG +
+
${Utils.escapeHtml(afterText)}
+
+
+ `; + Utils.scrollToBottom(this.chatHistory); + } // 完成流式消息 - finalizeStreamingMessage(messageId, fullContent) { + finalizeStreamingMessage(messageId, fullContent, svgId = null, beforeText = '') { const container = document.querySelector(`[data-message-id="${messageId}"]`); if (!container) return; - const parsed = Utils.parseSVGResponse(fullContent); - const message = { id: messageId, type: 'ai', @@ -269,26 +403,23 @@ class ProductCanvasApp { this.conversationHistory[this.currentMode].push(message); - // 如果包含SVG,存储SVG内容 - if (parsed.svgContent) { - const svgId = Utils.generateId('svg'); - this.svgStorage[this.currentMode][svgId] = { - content: parsed.svgContent, - messageId: messageId, - mode: this.currentMode, - timestamp: new Date().toISOString() - }; - - this.viewSVG(svgId); + // 如果已经有SVG ID(从流式处理中获得),直接使用 + if (svgId && this.svgStorage[this.currentMode][svgId]) { + // 提取SVG后的文本 + let afterText = ''; + if (fullContent.includes('')) { + const svgEndIndex = fullContent.indexOf('') + 6; + afterText = fullContent.substring(svgEndIndex); + } // 更新容器内容为包含SVG的消息 container.innerHTML = `
- ${Utils.escapeHtml(parsed.beforeText)} + ${Utils.escapeHtml(beforeText)}
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
- ${Utils.escapeHtml(parsed.afterText)} + ${Utils.escapeHtml(afterText)}
@@ -303,28 +434,68 @@ class ProductCanvasApp {
`; } else { - // 更新容器内容为普通消息 - container.innerHTML = ` -
- ${Utils.escapeHtml(fullContent)} -
+ // 使用原有的解析方法作为后备 + 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); + + // 更新容器内容为包含SVG的消息 + container.innerHTML = ` +
+ ${Utils.escapeHtml(parsed.beforeText)} +
+ 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG +
+ ${Utils.escapeHtml(parsed.afterText)} +
+ +
+ + +
+ `; + } else { + // 更新容器内容为普通消息 + container.innerHTML = ` +
+ ${Utils.escapeHtml(fullContent)} +
+ +
+ + +
+ `; + } } // 保存数据 - Utils.storage.set(`conversationHistory`, this.conversationHistory); - Utils.storage.set(`svgStorage`, this.svgStorage); + 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); } // 清空当前对话 @@ -351,8 +522,10 @@ class ProductCanvasApp { } // 保存数据 - Utils.storage.set('conversationHistory', this.conversationHistory); - Utils.storage.set('svgStorage', this.svgStorage); + 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(); @@ -371,7 +544,8 @@ class ProductCanvasApp { this.conversationHistory[this.currentMode].push(message); this.renderMessage(message); Utils.scrollToBottom(this.chatHistory); - Utils.storage.set('conversationHistory', this.conversationHistory); + Utils.storage.set('canvasHistory', this.conversationHistory.canvas); + Utils.storage.set('swotHistory', this.conversationHistory.swot); } // 添加AI消息(非流式,保留用于错误情况) @@ -398,7 +572,8 @@ class ProductCanvasApp { timestamp: new Date().toISOString() }; - Utils.storage.set('svgStorage', this.svgStorage); + Utils.storage.set('canvasSVGs', this.svgStorage.canvas); + Utils.storage.set('swotSVGs', this.svgStorage.swot); this.viewSVG(svgId); // 渲染包含SVG占位符的消息 @@ -409,7 +584,8 @@ class ProductCanvasApp { } Utils.scrollToBottom(this.chatHistory); - Utils.storage.set('conversationHistory', this.conversationHistory); + Utils.storage.set('canvasHistory', this.conversationHistory.canvas); + Utils.storage.set('swotHistory', this.conversationHistory.swot); } // 添加错误消息 @@ -425,7 +601,8 @@ class ProductCanvasApp { this.conversationHistory[this.currentMode].push(message); this.renderMessage(message); Utils.scrollToBottom(this.chatHistory); - Utils.storage.set('conversationHistory', this.conversationHistory); + Utils.storage.set('canvasHistory', this.conversationHistory.canvas); + Utils.storage.set('swotHistory', this.conversationHistory.swot); } // 渲染消息 @@ -506,13 +683,17 @@ class ProductCanvasApp { renderConversationHistory() { this.chatHistory.innerHTML = ''; - for (const message of this.conversationHistory) { + // 获取当前模式的对话历史 + const currentHistory = this.conversationHistory[this.currentMode] || []; + + 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(this.svgStorage)) { + const currentSvgStorage = this.svgStorage[this.currentMode] || {}; + for (const [id, svg] of Object.entries(currentSvgStorage)) { if (svg.messageId === message.id) { svgId = id; break; @@ -532,29 +713,29 @@ class ProductCanvasApp { // 显示SVG viewSVG(svgId) { - if (!this.svgStorage[svgId]) { + if (!this.svgStorage[this.currentMode][svgId]) { console.error('SVG not found:', svgId); return; } this.currentSvgId = svgId; - const svgContent = this.svgStorage[svgId].content; + const svgContent = this.svgStorage[this.currentMode][svgId].content; this.svgViewer.innerHTML = svgContent; } // 退回到指定消息 rollbackToMessage(messageId) { - const messageIndex = this.conversationHistory.findIndex(msg => msg.id === messageId); + const messageIndex = this.conversationHistory[this.currentMode].findIndex(msg => msg.id === messageId); if (messageIndex === -1) return; // 删除指定消息之后的所有消息 - const messagesToRemove = this.conversationHistory.slice(messageIndex + 1); + const messagesToRemove = this.conversationHistory[this.currentMode].slice(messageIndex + 1); // 删除相关的SVG for (const message of messagesToRemove) { - for (const [svgId, svg] of Object.entries(this.svgStorage)) { + for (const [svgId, svg] of Object.entries(this.svgStorage[this.currentMode])) { if (svg.messageId === message.id) { - delete this.svgStorage[svgId]; + delete this.svgStorage[this.currentMode][svgId]; // 如果当前显示的是被删除的SVG,清空显示 if (this.currentSvgId === svgId) { @@ -571,11 +752,13 @@ class ProductCanvasApp { } // 更新对话历史 - this.conversationHistory = this.conversationHistory.slice(0, messageIndex + 1); + this.conversationHistory[this.currentMode] = this.conversationHistory[this.currentMode].slice(0, messageIndex + 1); // 保存数据 - Utils.storage.set('conversationHistory', this.conversationHistory); - Utils.storage.set('svgStorage', this.svgStorage); + 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(); @@ -591,7 +774,7 @@ class ProductCanvasApp { try { // 重新生成响应 - const response = await window.apiClient.regenerateResponse(messageId, this.conversationHistory); + const response = await window.apiClient.regenerateResponse(messageId, this.conversationHistory[this.currentMode]); // 退回到指定消息 this.rollbackToMessage(messageId); @@ -616,7 +799,7 @@ class ProductCanvasApp { return; } - const svgContent = this.svgStorage[this.currentSvgId].content; + const svgContent = this.svgStorage[this.currentMode][this.currentSvgId].content; const filename = `${this.currentMode}-${Utils.formatDateTime().replace(/[/:]/g, '-')}.svg`; Utils.downloadFile(svgContent, filename, 'image/svg+xml'); } @@ -640,7 +823,7 @@ class ProductCanvasApp { return; } - const svgContent = this.svgStorage[this.currentSvgId].content; + const svgContent = this.svgStorage[this.currentMode][this.currentSvgId].content; // 创建代码查看模态窗 const modal = document.createElement('div');