diff --git a/css/style.css b/css/style.css index f0726b0..63d6d11 100644 --- a/css/style.css +++ b/css/style.css @@ -1,351 +1,347 @@ -body { - font-family: 'Inter', sans-serif; -} - -/* 狂野线条效果 */ -.wild-border { - border: 3px solid; - box-shadow: 4px 4px 0px rgba(0,0,0,0.3); -} - -/* 切换按钮激活状态 */ -.mode-btn-active { - transform: translateY(-2px); - box-shadow: 0 4px 0 rgba(0,0,0,0.3); -} - -.mode-btn-inactive { - opacity: 0.6; -} - -/* 对话气泡样式 */ -.chat-bubble-user { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 10px 14px; - max-width: 80%; - border: 2px solid #000; - box-shadow: 2px 2px 0 rgba(0,0,0,0.2); -} - -.chat-bubble-ai { - background: #fff; - 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); -} - -/* Markdown样式 */ -.chat-bubble-ai h1 { - font-size: 1.5em; - font-weight: bold; - margin: 0.5em 0; - color: #1f2937; -} - -.chat-bubble-ai h2 { - font-size: 1.3em; - font-weight: bold; - margin: 0.5em 0; - color: #1f2937; -} - -.chat-bubble-ai h3 { - font-size: 1.1em; - font-weight: bold; - margin: 0.5em 0; - color: #1f2937; -} - -.chat-bubble-ai p { - margin: 0.5em 0; - line-height: 1.5; -} - -.chat-bubble-ai ul, .chat-bubble-ai ol { - margin: 0.5em 0; - padding-left: 1.5em; -} - -.chat-bubble-ai li { - margin: 0.25em 0; - line-height: 1.4; -} - -.chat-bubble-ai code { - background: #f3f4f6; - padding: 0.2em 0.4em; - border-radius: 3px; - font-family: 'Courier New', monospace; - font-size: 0.9em; - color: #e11d48; -} - -.chat-bubble-ai pre { - background: #f9fafb; - border: 1px solid #e5e7eb; - border-radius: 4px; - padding: 0.75em; - overflow-x: auto; - margin: 0.5em 0; -} - -.chat-bubble-ai pre code { - background: none; - padding: 0; - color: #1f2937; -} - -.chat-bubble-ai blockquote { - border-left: 4px solid #d1d5db; - padding-left: 1em; - margin: 0.5em 0; - color: #6b7280; - font-style: italic; -} - -.chat-bubble-ai strong { - font-weight: bold; - color: #1f2937; -} - -.chat-bubble-ai em { - font-style: italic; - color: #4b5563; -} - -.chat-bubble-ai a { - color: #3b82f6; - text-decoration: underline; -} - -.chat-bubble-ai a:hover { - color: #1d4ed8; -} - -.chat-bubble-ai table { - border-collapse: collapse; - width: 100%; - margin: 0.5em 0; -} - -.chat-bubble-ai th, .chat-bubble-ai td { - border: 1px solid #e5e7eb; - padding: 0.5em; - text-align: left; -} - -.chat-bubble-ai th { - background: #f9fafb; - font-weight: bold; -} - -/* SVG占位符样式 - 块级换行 + 新配色 */ -.svg-placeholder-block { - display: block; - background: linear-gradient(135deg, #f59e0b 0%, #ef4444 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; -} - -.svg-placeholder-block:hover { - transform: translateX(2px) translateY(-2px); - box-shadow: 4px 4px 0 rgba(0,0,0,0.3); - 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; - transition: opacity 0.2s; -} - -.chat-bubble-ai:hover .bubble-action-btn { - opacity: 1; -} - -/* 小手摇摆动画 */ -@keyframes wave { - 0%, 100% {transform: translateX(0px) rotate(90deg);} - 10%, 30%, 50%, 70%, 90% {transform: translateX(-1px) rotate(90deg);} - 20%, 40%, 60%, 80% {transform: translateX(1px) rotate(90deg);} -} - -.wave-hand { - animation: wave 3s ease-in-out infinite; - display: inline-block; - transform: rotate(90deg); -} - -/* 模态窗样式 */ -.modal-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.7); - z-index: 1000; - align-items: center; - justify-content: center; -} - -.modal-overlay.active { - display: flex; -} - -.modal-content { - background: white; - border: 4px solid #000; - box-shadow: 8px 8px 0 rgba(0,0,0,0.4); - max-width: 500px; - width: 90%; - max-height: 90vh; - overflow-y: auto; -} - -/* 表单输入框样式 */ -.config-input { - width: 100%; - padding: 10px; - border: 2px solid #000; - font-size: 14px; - transition: all 0.2s; -} - -.config-input:focus { - outline: none; - border-color: #667eea; - box-shadow: 3px 3px 0 rgba(102, 126, 234, 0.3); -} - -/* 齿轮旋转动画 */ -@keyframes rotate { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -.settings-btn:hover iconify-icon { - animation: rotate 1s linear infinite; -} - -/* 自适应高度输入框 */ -.auto-resize-input { - min-height: 40px; - max-height: 120px; /* 5行左右的高度 */ - resize: none; - overflow-y: auto; - line-height: 1.5; - padding: 8px 12px; -} - -/* 清空按钮样式 */ -.clear-btn { - transition: all 0.2s; -} - -.clear-btn:hover { - transform: scale(1.05); -} - -.clear-btn:active { - transform: scale(0.95); -} - -/* 流式输出动画 */ -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -.streaming-text { - animation: fadeIn 0.3s ease-in-out; -} - -/* 打字机效果光标 */ -@keyframes blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0; } -} - -.typing-cursor::after { - content: '|'; - 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; +body { + font-family: 'Inter', sans-serif; +} + +/* 狂野线条效果 */ +.wild-border { + border: 3px solid; + box-shadow: 4px 4px 0px rgba(0,0,0,0.3); +} + +/* 切换按钮激活状态 */ +.mode-btn-active { + transform: translateY(-2px); + box-shadow: 0 4px 0 rgba(0,0,0,0.3); +} + +.mode-btn-inactive { + opacity: 0.6; +} + +/* 对话气泡样式 */ +.chat-bubble-user { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 10px 14px; + max-width: 80%; + border: 2px solid #000; + box-shadow: 2px 2px 0 rgba(0,0,0,0.2); +} + +.chat-bubble-ai { + background: #fff; + 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); +} + +/* Markdown样式 */ +.chat-bubble-ai h1 { + font-size: 1.5em; + font-weight: bold; + margin: 0.5em 0; + color: #1f2937; +} + +.chat-bubble-ai h2 { + font-size: 1.3em; + font-weight: bold; + margin: 0.5em 0; + color: #1f2937; +} + +.chat-bubble-ai h3 { + font-size: 1.1em; + font-weight: bold; + margin: 0.5em 0; + color: #1f2937; +} + +.chat-bubble-ai p { + margin: 0.5em 0; + line-height: 1.5; +} + +.chat-bubble-ai ul, .chat-bubble-ai ol { + margin: 0.5em 0; + padding-left: 1.5em; +} + +.chat-bubble-ai li { + margin: 0.25em 0; + line-height: 1.4; +} + +.chat-bubble-ai code { + background: #f3f4f6; + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.9em; + color: #e11d48; +} + +.chat-bubble-ai pre { + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 0.75em; + overflow-x: auto; + margin: 0.5em 0; +} + +.chat-bubble-ai pre code { + background: none; + padding: 0; + color: #1f2937; +} + +.chat-bubble-ai blockquote { + border-left: 4px solid #d1d5db; + padding-left: 1em; + margin: 0.5em 0; + color: #6b7280; + font-style: italic; +} + +.chat-bubble-ai strong { + font-weight: bold; + color: #1f2937; +} + +.chat-bubble-ai em { + font-style: italic; + color: #4b5563; +} + +.chat-bubble-ai a { + color: #3b82f6; + text-decoration: underline; +} + +.chat-bubble-ai a:hover { + color: #1d4ed8; +} + +.chat-bubble-ai table { + border-collapse: collapse; + width: 100%; + margin: 0.5em 0; +} + +.chat-bubble-ai th, .chat-bubble-ai td { + border: 1px solid #e5e7eb; + padding: 0.5em; + text-align: left; +} + +.chat-bubble-ai th { + background: #f9fafb; + font-weight: bold; +} + +/* SVG占位符样式 - 块级换行 + 新配色 */ +.svg-placeholder-block { + display: block; + background: linear-gradient(135deg, #f59e0b 0%, #ef4444 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; +} + +.svg-placeholder-block:hover { + transform: translateX(2px) translateY(-2px); + box-shadow: 4px 4px 0 rgba(0,0,0,0.3); + 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: 1; + transition: color 0.2s ease; +} + +/* 小手摇摆动画 */ +@keyframes wave { + 0%, 100% {transform: translateX(0px) rotate(90deg);} + 10%, 30%, 50%, 70%, 90% {transform: translateX(-1px) rotate(90deg);} + 20%, 40%, 60%, 80% {transform: translateX(1px) rotate(90deg);} +} + +.wave-hand { + animation: wave 3s ease-in-out infinite; + display: inline-block; + transform: rotate(90deg); +} + +/* 模态窗样式 */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal-overlay.active { + display: flex; +} + +.modal-content { + background: white; + border: 4px solid #000; + box-shadow: 8px 8px 0 rgba(0,0,0,0.4); + max-width: 500px; + width: 90%; + max-height: 90vh; + overflow-y: auto; +} + +/* 表单输入框样式 */ +.config-input { + width: 100%; + padding: 10px; + border: 2px solid #000; + font-size: 14px; + transition: all 0.2s; +} + +.config-input:focus { + outline: none; + border-color: #667eea; + box-shadow: 3px 3px 0 rgba(102, 126, 234, 0.3); +} + +/* 齿轮旋转动画 */ +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.settings-btn:hover iconify-icon { + animation: rotate 1s linear infinite; +} + +/* 自适应高度输入框 */ +.auto-resize-input { + min-height: 40px; + max-height: 120px; /* 5行左右的高度 */ + resize: none; + overflow-y: auto; + line-height: 1.5; + padding: 8px 12px; +} + +/* 清空按钮样式 */ +.clear-btn { + transition: all 0.2s; +} + +.clear-btn:hover { + transform: scale(1.05); +} + +.clear-btn:active { + transform: scale(0.95); +} + +/* 流式输出动画 */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.streaming-text { + animation: fadeIn 0.3s ease-in-out; +} + +/* 打字机效果光标 */ +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0; } +} + +.typing-cursor::after { + content: '|'; + 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/js/apiclient.js b/js/apiclient.js index a6b7e5c..a7db5f9 100644 --- a/js/apiclient.js +++ b/js/apiclient.js @@ -1,299 +1,299 @@ -/** - * API客户端 - 处理与AI服务的交互 - */ - -class APIClient { - constructor() { - this.config = { - url: '', - key: '', - model: '' - }; - this.prompts = { - canvas: '', - swot: '' - }; - this.loadConfig(); - this.loadPrompts(); - } - - // 加载API配置 - loadConfig() { - const savedConfig = Utils.storage.get('apiConfig'); - if (savedConfig) { - this.config = { ...this.config, ...savedConfig }; - } - } - - // 加载系统提示词 - async loadPrompts() { - try { - // 加载产品画布提示词 - const canvasResponse = await fetch('prompts/canvas-prompt.txt'); - this.prompts.canvas = await canvasResponse.text(); - - // 加载SWOT分析提示词 - const swotResponse = await fetch('prompts/swot-prompt.txt'); - this.prompts.swot = await swotResponse.text(); - } catch (error) { - console.error('加载提示词失败:', error); - // 使用默认提示词 - this.prompts.canvas = '你是一个专业的产品战略分析师,擅长创建产品画布。'; - this.prompts.swot = '你是一个专业的商业战略分析师,擅长进行SWOT分析。'; - } - } - - // 保存API配置 - saveConfig(config) { - this.config = { ...this.config, ...config }; - return Utils.storage.set('apiConfig', this.config); - } - - // 获取当前配置 - getConfig() { - return { ...this.config }; - } - - // 验证配置是否完整 - isConfigValid() { - return this.config.url && this.config.key && this.config.model; - } - - // 测试API连接 - async testConnection() { - if (!this.isConfigValid()) { - throw new Error('API配置不完整,请填写所有字段'); - } - - try { - const response = await this.makeRequest([ - { role: 'user', content: '测试连接' } - ], 5); - - return { success: true, data: response }; - } catch (error) { - throw new Error(`连接测试失败: ${error.message}`); - } - } - - // 发送聊天请求 - async sendChatMessage(messages, options = {}) { - if (!this.isConfigValid()) { - throw new Error('API配置不完整,请先配置API设置'); - } - - const maxTokens = options.maxTokens || 2000; - const temperature = options.temperature || 0.7; - - try { - const response = await this.makeRequest(messages, maxTokens, temperature); - return response; - } catch (error) { - throw new Error(`API请求失败: ${error.message}`); - } - } - - // 核心请求方法 - async makeRequest(messages, maxTokens, temperature = 0.7) { - const requestBody = { - model: this.config.model, - messages: messages, - max_tokens: maxTokens, - temperature: temperature - }; - - const response = await fetch(this.config.url, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.config.key}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestBody) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - - if (!data.choices || !data.choices.length || !data.choices[0].message) { - throw new Error('API返回数据格式异常'); - } - - return data.choices[0].message.content; - } - - // 生成产品画布的专用方法 - async generateProductCanvas(userRequest, context = []) { - const messages = [ - { role: 'system', content: this.prompts.canvas }, - ...context, - { role: 'user', content: userRequest } - ]; - - return await this.sendChatMessage(messages, { maxTokens: 18000 }); - } - - // 生成SWOT分析的专用方法 - async generateSWOTAnalysis(userRequest, context = []) { - const messages = [ - { role: 'system', content: this.prompts.swot }, - ...context, - { role: 'user', content: userRequest } - ]; - - return await this.sendChatMessage(messages, { maxTokens: 18000 }); - } - - // 流式生成产品画布 - async generateProductCanvasStream(userRequest, context = [], onChunk, onComplete) { - const messages = [ - { role: 'system', content: this.prompts.canvas }, - ...context, - { role: 'user', content: userRequest } - ]; - - return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete); - } - - // 流式生成SWOT分析 - async generateSWOTAnalysisStream(userRequest, context = [], onChunk, onComplete) { - const messages = [ - { role: 'system', content: this.prompts.swot }, - ...context, - { role: 'user', content: userRequest } - ]; - - return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete); - } - - // 流式发送聊天请求 - async sendChatMessageStream(messages, options = {}, onChunk, onComplete) { - if (!this.isConfigValid()) { - throw new Error('API配置不完整,请先配置API设置'); - } - - const maxTokens = options.maxTokens || 2000; - const temperature = options.temperature || 0.7; - const stream = true; - - const requestBody = { - model: this.config.model, - messages: messages, - max_tokens: maxTokens, - temperature: temperature, - stream: stream - }; - - const url = this.config.url.replace('/chat/completions', '/chat/completions'); - - try { - await Utils.createStreamRequest( - url, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.config.key}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestBody) - }, - onChunk, - onComplete - ); - } catch (error) { - throw new Error(`流式API请求失败: ${error.message}`); - } - } - - // 重新生成响应 - async regenerateResponse(messageId, conversationHistory) { - // 找到指定消息ID之前的所有对话历史 - const contextMessages = conversationHistory - .filter(msg => msg.id <= messageId) - .map(msg => ({ - role: msg.type === 'user' ? 'user' : 'assistant', - content: msg.content - })); - - if (contextMessages.length === 0) { - throw new Error('没有找到有效的对话上下文'); - } - - // 移除最后一条消息(需要重新生成的消息) - if (contextMessages.length > 0 && contextMessages[contextMessages.length - 1].role === 'assistant') { - contextMessages.pop(); - } - - // 根据当前模式选择相应的生成方法 - const lastUserMessage = contextMessages.filter(msg => msg.role === 'user').pop(); - if (!lastUserMessage) { - throw new Error('没有找到用户消息'); - } - - const mode = Utils.storage.get('currentMode', 'canvas'); - - if (mode === 'canvas') { - return await this.generateProductCanvas(lastUserMessage.content, contextMessages.slice(0, -1)); - } else { - return await this.generateSWOTAnalysis(lastUserMessage.content, contextMessages.slice(0, -1)); - } - } - - // 模拟API响应(用于测试) - simulateAPIResponse(userMessage, mode = 'canvas') { - return new Promise((resolve) => { - setTimeout(() => { - const mockResponses = [ - `好的!我为您生成了一个${mode === 'canvas' ? '产品画布' : 'SWOT分析'} -\`\`\`svg - - - - - - - - - - - 这是${mode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG - - - - -\`\`\` -包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`, - - `已经为您调整完成! -\`\`\`svg - - - - - - - - - - - ${mode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'} - - - - -\`\`\` -采用了更加鲜明的色彩组合,希望您满意!` - ]; - - const response = mockResponses[Math.floor(Math.random() * mockResponses.length)]; - resolve(response); - }, 1000 + Math.random() * 1000); // 1-2秒的随机延迟 - }); - } -} - -// 创建全局API客户端实例 +/** + * API客户端 - 处理与AI服务的交互 + */ + +class APIClient { + constructor() { + this.config = { + url: '', + key: '', + model: '' + }; + this.prompts = { + canvas: '', + swot: '' + }; + this.loadConfig(); + this.loadPrompts(); + } + + // 加载API配置 + loadConfig() { + const savedConfig = Utils.storage.get('apiConfig'); + if (savedConfig) { + this.config = { ...this.config, ...savedConfig }; + } + } + + // 加载系统提示词 + async loadPrompts() { + try { + // 加载产品画布提示词 + const canvasResponse = await fetch('prompts/canvas-prompt.txt'); + this.prompts.canvas = await canvasResponse.text(); + + // 加载SWOT分析提示词 + const swotResponse = await fetch('prompts/swot-prompt.txt'); + this.prompts.swot = await swotResponse.text(); + } catch (error) { + console.error('加载提示词失败:', error); + // 使用默认提示词 + this.prompts.canvas = '你是一个专业的产品战略分析师,擅长创建产品画布。'; + this.prompts.swot = '你是一个专业的商业战略分析师,擅长进行SWOT分析。'; + } + } + + // 保存API配置 + saveConfig(config) { + this.config = { ...this.config, ...config }; + return Utils.storage.set('apiConfig', this.config); + } + + // 获取当前配置 + getConfig() { + return { ...this.config }; + } + + // 验证配置是否完整 + isConfigValid() { + return this.config.url && this.config.key && this.config.model; + } + + // 测试API连接 + async testConnection() { + if (!this.isConfigValid()) { + throw new Error('API配置不完整,请填写所有字段'); + } + + try { + const response = await this.makeRequest([ + { role: 'user', content: '测试连接' } + ], 5); + + return { success: true, data: response }; + } catch (error) { + throw new Error(`连接测试失败: ${error.message}`); + } + } + + // 发送聊天请求 + async sendChatMessage(messages, options = {}) { + if (!this.isConfigValid()) { + throw new Error('API配置不完整,请先配置API设置'); + } + + const maxTokens = options.maxTokens || 2000; + const temperature = options.temperature || 0.7; + + try { + const response = await this.makeRequest(messages, maxTokens, temperature); + return response; + } catch (error) { + throw new Error(`API请求失败: ${error.message}`); + } + } + + // 核心请求方法 + async makeRequest(messages, maxTokens, temperature = 0.7) { + const requestBody = { + model: this.config.model, + messages: messages, + max_tokens: maxTokens, + temperature: temperature + }; + + const response = await fetch(this.config.url, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.config.key}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (!data.choices || !data.choices.length || !data.choices[0].message) { + throw new Error('API返回数据格式异常'); + } + + return data.choices[0].message.content; + } + + // 生成产品画布的专用方法 + async generateProductCanvas(userRequest, context = []) { + const messages = [ + { role: 'system', content: this.prompts.canvas }, + ...context, + { role: 'user', content: userRequest } + ]; + + return await this.sendChatMessage(messages, { maxTokens: 18000 }); + } + + // 生成SWOT分析的专用方法 + async generateSWOTAnalysis(userRequest, context = []) { + const messages = [ + { role: 'system', content: this.prompts.swot }, + ...context, + { role: 'user', content: userRequest } + ]; + + return await this.sendChatMessage(messages, { maxTokens: 18000 }); + } + + // 流式生成产品画布 + async generateProductCanvasStream(userRequest, context = [], onChunk, onComplete) { + const messages = [ + { role: 'system', content: this.prompts.canvas }, + ...context, + { role: 'user', content: userRequest } + ]; + + return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete); + } + + // 流式生成SWOT分析 + async generateSWOTAnalysisStream(userRequest, context = [], onChunk, onComplete) { + const messages = [ + { role: 'system', content: this.prompts.swot }, + ...context, + { role: 'user', content: userRequest } + ]; + + return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete); + } + + // 流式发送聊天请求 + async sendChatMessageStream(messages, options = {}, onChunk, onComplete) { + if (!this.isConfigValid()) { + throw new Error('API配置不完整,请先配置API设置'); + } + + const maxTokens = options.maxTokens || 2000; + const temperature = options.temperature || 0.7; + const stream = true; + + const requestBody = { + model: this.config.model, + messages: messages, + max_tokens: maxTokens, + temperature: temperature, + stream: stream + }; + + const url = this.config.url.replace('/chat/completions', '/chat/completions'); + + try { + return Utils.createStreamRequest( + url, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.config.key}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }, + onChunk, + onComplete + ); + } catch (error) { + throw new Error(`流式API请求失败: ${error.message}`); + } + } + + // 重新生成响应 + async regenerateResponse(messageId, conversationHistory) { + // 找到指定消息ID之前的所有对话历史 + const contextMessages = conversationHistory + .filter(msg => msg.id <= messageId) + .map(msg => ({ + role: msg.type === 'user' ? 'user' : 'assistant', + content: msg.content + })); + + if (contextMessages.length === 0) { + throw new Error('没有找到有效的对话上下文'); + } + + // 移除最后一条消息(需要重新生成的消息) + if (contextMessages.length > 0 && contextMessages[contextMessages.length - 1].role === 'assistant') { + contextMessages.pop(); + } + + // 根据当前模式选择相应的生成方法 + const lastUserMessage = contextMessages.filter(msg => msg.role === 'user').pop(); + if (!lastUserMessage) { + throw new Error('没有找到用户消息'); + } + + const mode = Utils.storage.get('currentMode', 'canvas'); + + if (mode === 'canvas') { + return await this.generateProductCanvas(lastUserMessage.content, contextMessages.slice(0, -1)); + } else { + return await this.generateSWOTAnalysis(lastUserMessage.content, contextMessages.slice(0, -1)); + } + } + + // 模拟API响应(用于测试) + simulateAPIResponse(userMessage, mode = 'canvas') { + return new Promise((resolve) => { + setTimeout(() => { + const mockResponses = [ + `好的!我为您生成了一个${mode === 'canvas' ? '产品画布' : 'SWOT分析'} +\`\`\`svg + + + + + + + + + + + 这是${mode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG + + + + +\`\`\` +包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`, + + `已经为您调整完成! +\`\`\`svg + + + + + + + + + + + ${mode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'} + + + + +\`\`\` +采用了更加鲜明的色彩组合,希望您满意!` + ]; + + const response = mockResponses[Math.floor(Math.random() * mockResponses.length)]; + resolve(response); + }, 1000 + Math.random() * 1000); // 1-2秒的随机延迟 + }); + } +} + +// 创建全局API客户端实例 window.apiClient = new APIClient(); \ No newline at end of file diff --git a/js/app.js b/js/app.js index cfa09d5..38c1b7e 100644 --- a/js/app.js +++ b/js/app.js @@ -1,31 +1,32 @@ -/** - * 应用核心逻辑 - */ - -// 配置Markdown解析器 -if (typeof marked !== 'undefined') { - marked.setOptions({ - breaks: true, // 支持换行 - gfm: true, // 支持GitHub风格的Markdown - sanitize: false, // 允许HTML(因为我们自己处理SVG) - smartLists: true, // 智能列表 - smartypants: true // 智能标点 - }); -} - -class ProductCanvasApp { +/** + * 应用核心逻辑 + */ + +// 配置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.currentStreamingMessage = null; - - this.initElements(); - this.initEventListeners(); - this.loadSavedData(); + this.isProcessing = false; + this.activeStreamHandle = null; + + this.initElements(); + this.initEventListeners(); + this.loadSavedData(); this.updateModeUI(); + this.setSendButtonState('idle'); } getModeDisplayName(mode = this.currentMode) { @@ -34,112 +35,120 @@ class ProductCanvasApp { // 初始化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 = document.getElementById('placeholder-text'); - - // 底部操作按钮 - this.downloadSvgBtn = document.getElementById('download-svg-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', () => this.sendMessage()); - this.clearHistoryBtn.addEventListener('click', () => this.clearCurrentConversation()); - - // 输入框事件 - this.chatInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - this.sendMessage(); - } - }); - - // 自动调整输入框高度 - this.chatInput.addEventListener('input', () => { - Utils.autoResizeTextarea(this.chatInput); - }); - - // 底部操作按钮 - this.downloadSvgBtn.addEventListener('click', () => this.downloadSVG()); - 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.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.downloadSvgBtn = document.getElementById('download-svg-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); + }); + + // 底部操作按钮 + this.downloadSvgBtn.addEventListener('click', () => this.downloadSVG()); + 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, + this.svgStorage = { + canvas: savedCanvasSVGs, swot: savedSwotSVGs }; this.renderSvgViewerForMode(); // 加载API配置 - const apiConfig = window.apiClient.getConfig(); - this.apiUrlInput.value = apiConfig.url || ''; - this.apiKeyInput.value = apiConfig.key || ''; - this.apiModelInput.value = apiConfig.model || ''; - } - + 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; @@ -147,36 +156,76 @@ class ProductCanvasApp { this.currentMode = mode; Utils.storage.set('currentMode', mode); this.currentSvgId = null; - this.currentMode = mode; - Utils.storage.set('currentMode', mode); + this.showSvgPlaceholder(); this.updateModeUI(); this.renderConversationHistory(); this.renderSvgViewerForMode(); } - - // 更新模式UI + + // 更新模式UI updateModeUI() { - if (this.currentMode === 'canvas') { + 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'); - this.pageTitle.textContent = '产品画布'; - if (!this.currentSvgId) { - this.placeholderText.textContent = '生成的产品画布将在此处显示'; - } + 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 = 'SWOT分析'; - if (!this.currentSvgId) { - this.placeholderText.textContent = '生成的SWOT分析将在此处显示'; + 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; @@ -186,6 +235,7 @@ class ProductCanvasApp {

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

`; + this.placeholderText = this.svgViewer.querySelector('#placeholder-text'); } renderSvgViewerForMode() { @@ -208,259 +258,337 @@ class ProductCanvasApp { if (latestSvgId && svgStore[latestSvgId]) { this.currentSvgId = latestSvgId; this.svgViewer.innerHTML = svgStore[latestSvgId].content; + this.placeholderText = null; } else { this.showSvgPlaceholder(); } } - - // 发送消息 - async sendMessage() { - const message = this.chatInput.value.trim(); - if (!message || this.isProcessing) return; - - // 检查API配置 - if (!window.apiClient.isConfigValid()) { - alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。'); - this.openConfigModal(); - return; - } - - this.isProcessing = true; - this.sendButton.disabled = true; - this.sendButton.innerHTML = ''; - - // 添加用户消息 - this.addUserMessage(message); - this.chatInput.value = ''; - Utils.autoResizeTextarea(this.chatInput); - - try { - // 获取对话上下文 - const contextMessages = this.conversationHistory[this.currentMode] - .slice(-10) // 只取最近10条消息作为上下文 - .map(msg => ({ - role: msg.type === 'user' ? 'user' : 'assistant', - content: msg.content - })); - - // 开始流式接收消息 - await this.startStreamingMessage(message, contextMessages); - - } catch (error) { - console.error('发送消息失败:', error); - this.addErrorMessage(error.message); - this.isProcessing = false; - this.sendButton.disabled = false; - this.sendButton.innerHTML = ''; - } - } - - // 开始流式接收消息 - 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 = ''; - - const onChunk = (chunk) => { - if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) { - const content = chunk.choices[0].delta.content || ''; - fullContent += content; - - // 检测SVG开始标记 - if (!svgStarted) { - // 使用正则表达式更准确地检测SVG代码块开始 - const svgStartMatch = fullContent.match(/```(?:svg)?\s*/i); - if (svgStartMatch) { - svgStarted = true; - svgId = Utils.generateId('svg'); - - // 提取SVG开始前的文本 - const svgStartIndex = svgStartMatch.index; - beforeText = fullContent.substring(0, svgStartIndex); - - // 显示绘制中占位符 - this.updateStreamingMessageWithPlaceholder(messageContainer, beforeText, svgId); - - // 初始化SVG显示区域 - this.svgViewer.innerHTML = ` -
-
- -

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

-
-
- `; - } - } - - // 如果SVG已经开始,收集SVG内容 - if (svgStarted) { - // 检查是否有SVG结束标记 - if (fullContent.includes('')) { - const svgEndIndex = fullContent.indexOf('') + 6; // +6 是 '' 的长度 - - // 提取完整的SVG内容 - 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(); - - // 补全SVG结束标签(如果没有的话) - if (!svgContent.endsWith('')) { - 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 { - // SVG还在继续,更新内容 - 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(); - - // 补全SVG结束标签以便实时显示 - let tempSvgContent = svgContent; - if (!tempSvgContent.endsWith('')) { - tempSvgContent += ''; - } - - // 实时更新SVG显示 - this.svgViewer.innerHTML = tempSvgContent; - } - } - } else { - // 普通文本更新 - this.updateStreamingMessage(messageContainer, fullContent); - } - } - }; - - const onComplete = () => { - // 流式接收完成,处理完整消息 - this.finalizeStreamingMessage(messageId, fullContent, svgId); + + // 发送消息 + async sendMessage() { + const message = this.chatInput.value.trim(); + if (!message || this.isProcessing) return; + + // 检查API配置 + if (!window.apiClient.isConfigValid()) { + alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。'); + this.openConfigModal(); + return; + } + + this.isProcessing = true; + this.setSendButtonState('busy'); + this.sendButton.disabled = true; + + // 添加用户消息 + this.addUserMessage(message); + this.chatInput.value = ''; + Utils.autoResizeTextarea(this.chatInput); + + try { + // 获取对话上下文 + const contextMessages = this.conversationHistory[this.currentMode] + .slice(-10) // 只取最近10条消息作为上下文 + .map(msg => ({ + role: msg.type === 'user' ? 'user' : 'assistant', + content: msg.content + })); + // 开始流式接收消息 + await this.startStreamingMessage(message, contextMessages); + + } catch (error) { + console.error('发送消息失败:', error); + this.addErrorMessage(error.message); this.isProcessing = false; - this.sendButton.disabled = false; - this.sendButton.innerHTML = ''; + this.setSendButtonState('idle'); + this.activeStreamHandle = null; + } + } + + // 开始流式接收消息 + 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.activeStreamHandle = null; }; - - // 调用流式API - if (this.currentMode === 'canvas') { - await window.apiClient.generateProductCanvasStream(userMessage, contextMessages, onChunk, onComplete); - } else { - await window.apiClient.generateSWOTAnalysisStream(userMessage, contextMessages, onChunk, onComplete); - } - } - - // 创建流式消息容器 + + 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'); + + const svgStartIndex = svgStartMatch.index; + beforeText = fullContent.substring(0, svgStartIndex); + + this.updateStreamingMessageWithPlaceholder(messageContainer, beforeText, svgId); + + this.svgViewer.innerHTML = ` +
+
+ +

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

+
+
+ `; + } + } + + 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.svgViewer.innerHTML = svgContent; + + this.svgStorage[this.currentMode][svgId] = { + content: svgContent, + messageId, + mode: this.currentMode, + timestamp: new Date().toISOString() + }; + + 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.svgViewer.innerHTML = 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; + + await streamHandle.finished; + + if (!streamClosed) { + finalizeStream({ aborted: false }); + } + } catch (error) { + streamClosed = true; + this.activeStreamHandle = null; + this.isProcessing = false; + this.setSendButtonState('idle'); + + 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') { + return; + } + + 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.innerHTML = `📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG`; - placeholder.setAttribute('onclick', `app.viewSVG('${svgId}')`); - } - } - + 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.innerHTML = `📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG`; + placeholder.setAttribute('onclick', `app.viewSVG('${svgId}')`); + } + } + // 更新SVG后的消息内容 updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) { // 使用Markdown解析文本 const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText); const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText); - - container.innerHTML = ` -
-
- ${parsedBeforeText} -
- 📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG -
-
${parsedAfterText}
-
-
+ + 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 = []; @@ -487,17 +615,6 @@ class ProductCanvasApp { // 完成流式消息 finalizeStreamingMessage(messageId, fullContent, svgId = null) { - let container = document.querySelector(`.chat-bubble-ai[data-message-id="${messageId}"]`); - if (!container) { - const fallback = document.querySelector(`[data-message-id="${messageId}"]`); - if (fallback) { - container = fallback.classList.contains('chat-bubble-ai') - ? fallback - : fallback.querySelector('.chat-bubble-ai'); - } - } - if (!container) return; - const parsed = Utils.parseSVGResponse(fullContent); const timestamp = new Date().toISOString(); @@ -508,28 +625,14 @@ class ProductCanvasApp { timestamp }; - container.classList.remove('streaming-text'); - container.setAttribute('data-message-id', messageId); - - const actionFooter = ` -
- - -
- `; + let targetSvgId = svgId || null; if (parsed.svgContent && parsed.svgContent.includes('') ? parsed.svgContent.trim() - : `${parsed.svgContent.trim()}\n`; + : `${parsed.svgContent.trim()} +`; - let targetSvgId = svgId || null; if (!targetSvgId || !this.svgStorage[this.currentMode][targetSvgId]) { targetSvgId = targetSvgId || Utils.generateId('svg'); } @@ -541,235 +644,182 @@ class ProductCanvasApp { timestamp }; - const beforeHtml = parsed.beforeText - ? (typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText)) - : ''; - const afterHtml = parsed.afterText - ? (typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText)) - : ''; - - container.innerHTML = ` -
- ${beforeHtml} -
- 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG -
- ${afterHtml} -
- ${actionFooter} - `; - this.currentSvgId = targetSvgId; this.svgViewer.innerHTML = svgBody; message.content = this.buildSVGMessageContent(parsed.beforeText, svgBody, parsed.afterText); } else { - const sanitizedText = fullContent.replace(/^\s*```/, '').replace(/```$/, '').trim(); - const parsedContent = sanitizedText - ? (typeof marked !== 'undefined' ? marked.parse(sanitizedText) : Utils.escapeHtml(sanitizedText)) - : ''; - - container.innerHTML = ` -
- ${parsedContent} -
- ${actionFooter} - `; - + 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.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); - Utils.scrollToBottom(this.chatHistory); + + // 重新渲染对话历史 + this.renderConversationHistory(); + this.renderSvgViewerForMode(); } - - // 清空当前对话 - clearCurrentConversation() { - if (!confirm(`确定要清空当前的${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}对话吗?`)) { - return; - } - - // 清空当前模式的对话历史 - this.conversationHistory[this.currentMode] = []; - - // 清空当前模式的SVG存储 - this.svgStorage[this.currentMode] = {}; - - // 如果当前显示的是被清空的模式的SVG,清空显示 - if (this.currentSvgId && this.svgStorage[this.currentMode][this.currentSvgId]) { - this.currentSvgId = null; - this.svgViewer.innerHTML = ` -
- -

生成的${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}将在此处显示

-
- `; - } - - // 保存数据 - 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(); - } - - // 添加用户消息 - 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); - - // 如果包含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() - }; - - Utils.storage.set('canvasSVGs', this.svgStorage.canvas); - Utils.storage.set('swotSVGs', this.svgStorage.swot); - this.viewSVG(svgId); - - // 渲染包含SVG占位符的消息 - this.renderMessageWithSVG(message, parsed, svgId); - } else { - // 渲染普通消息 - this.renderMessage(message); - } - - Utils.scrollToBottom(this.chatHistory); - Utils.storage.set('canvasHistory', this.conversationHistory.canvas); - Utils.storage.set('swotHistory', this.conversationHistory.swot); - } - - // 添加错误消息 - 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) { - 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 { - messageDiv.className = 'flex justify-start'; - messageDiv.innerHTML = ` -
-
- ${typeof marked !== 'undefined' ? marked.parse(message.content) : Utils.escapeHtml(message.content)} -
- -
- - -
-
- `; - } - - this.chatHistory.appendChild(messageDiv); - } - - // 渲染包含SVG的消息 - renderMessageWithSVG(message, parsed, svgId) { - const messageDiv = document.createElement('div'); - messageDiv.className = 'flex justify-start'; - messageDiv.innerHTML = ` -
-
- ${typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText)} -
- 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG -
- ${typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText)} -
- -
- - -
-
- `; - - this.chatHistory.appendChild(messageDiv); - } - + + // 添加用户消息 + 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'; + messageDiv.innerHTML = ` +
+
+ ${beforeHtml} +
+ 📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG +
+ ${afterHtml} +
+ ${actions} +
+ `; + + this.chatHistory.appendChild(messageDiv); + } + // 渲染对话历史 renderConversationHistory() { this.chatHistory.innerHTML = ''; @@ -779,7 +829,15 @@ class ProductCanvasApp { 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); @@ -811,11 +869,11 @@ class ProductCanvasApp { hasStorageUpdate = true; hasHistoryUpdate = true; } - this.renderMessageWithSVG(message, parsed, svgId); + this.renderMessageWithSVG(message, parsed, svgId, { allowRegenerate: message.id === lastAiMessageId }); continue; } - - this.renderMessage(message); + + this.renderMessage(message, { allowRegenerate: message.id === lastAiMessageId }); } else { this.renderMessage(message); } @@ -833,241 +891,236 @@ class ProductCanvasApp { Utils.scrollToBottom(this.chatHistory); } - - // 显示SVG - viewSVG(svgId) { - if (!this.svgStorage[this.currentMode][svgId]) { - console.error('SVG not found:', svgId); - return; - } - - this.currentSvgId = svgId; - const svgContent = this.svgStorage[this.currentMode][svgId].content; - this.svgViewer.innerHTML = svgContent; - } - - // 退回到指定消息 - 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.currentSvgId = null; - this.svgViewer.innerHTML = ` -
- -

生成的${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}将在此处显示

-
- `; - } - } - } - } - - // 更新对话历史 - 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(); - } - - // 重新生成消息 - async regenerateMessage(messageId) { - if (this.isProcessing) return; - - this.isProcessing = true; - this.sendButton.disabled = true; - this.sendButton.innerHTML = ''; - - try { - // 重新生成响应 - const response = await window.apiClient.regenerateResponse(messageId, this.conversationHistory[this.currentMode]); - - // 退回到指定消息 - this.rollbackToMessage(messageId); - - // 添加新的AI回复 - this.addAIMessage(response); - - } catch (error) { - console.error('重新生成失败:', error); - this.addErrorMessage(error.message); - } finally { - this.isProcessing = false; - this.sendButton.disabled = false; - this.sendButton.innerHTML = ''; - } - } - - // 下载SVG - downloadSVG() { - if (!this.currentSvgId) { - alert('请先生成SVG图表'); - return; - } - - 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'); - } - - // 导出为图片 - exportAsImage() { - if (!this.currentSvgId) { - alert('请先生成SVG图表'); - return; - } - - // 这里可以实现SVG转PNG的功能 - // 由于需要额外的库,这里先提示用户 - alert('SVG转PNG功能需要额外的库支持,您可以使用下载SVG功能,然后使用在线工具转换。'); - } - - // 查看SVG代码 - viewSVGCode() { - if (!this.currentSvgId) { - alert('请先生成SVG图表'); - return; - } - - const svgContent = this.svgStorage[this.currentMode][this.currentSvgId].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(); + + // 显示SVG + viewSVG(svgId) { + if (!this.svgStorage[this.currentMode][svgId]) { + console.error('SVG not found:', svgId); + return; + } + + this.currentSvgId = svgId; + const svgContent = this.svgStorage[this.currentMode][svgId].content; + this.svgViewer.innerHTML = svgContent; + } + + // 退回到指定消息 + 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; + + this.isProcessing = true; + this.setSendButtonState('busy'); + this.sendButton.disabled = true; + + try { + // 重新生成响应 + const response = await window.apiClient.regenerateResponse(messageId, this.conversationHistory[this.currentMode]); + + // 退回到指定消息 + this.rollbackToMessage(messageId); + + // 添加新的AI回复 + this.addAIMessage(response); + + } catch (error) { + console.error('重新生成失败:', error); + this.addErrorMessage(error.message); + } finally { + this.isProcessing = false; + this.setSendButtonState('idle'); + this.activeStreamHandle = null; + } + } + + // 下载SVG + downloadSVG() { + if (!this.currentSvgId) { + alert('请先生成SVG图表'); + return; + } + + 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'); + } + + // 导出为图片 + exportAsImage() { + if (!this.currentSvgId) { + alert('请先生成SVG图表'); + return; + } + + // 这里可以实现SVG转PNG的功能 + // 由于需要额外的库,这里先提示用户 + alert('SVG转PNG功能需要额外的库支持,您可以使用下载SVG功能,然后使用在线工具转换。'); + } + + // 查看SVG代码 + viewSVGCode() { + if (!this.currentSvgId) { + alert('请先生成SVG图表'); + return; + } + + const svgContent = this.svgStorage[this.currentMode][this.currentSvgId].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(); }); diff --git a/js/utils.js b/js/utils.js index 02b2b2b..64d28ad 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,26 +1,26 @@ -/** - * 工具函数集合 - */ - -// HTML转义,防止XSS攻击 -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -// 滚动到指定元素的底部 -function scrollToBottom(element) { - if (element) { - element.scrollTop = element.scrollHeight; - } -} - -// 生成唯一ID -function generateId(prefix = 'id') { - return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; -} - +/** + * 工具函数集合 + */ + +// HTML转义,防止XSS攻击 +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// 滚动到指定元素的底部 +function scrollToBottom(element) { + if (element) { + element.scrollTop = element.scrollHeight; + } +} + +// 生成唯一ID +function generateId(prefix = 'id') { + return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +} + // 解析SVG响应,提取SVG内容和前后文本,容错缺失的结束反引号 function parseSVGResponse(response = '') { const content = typeof response === 'string' ? response : String(response || ''); @@ -78,246 +78,275 @@ function parseSVGResponse(response = '') { afterText: '' }; } - -// 下载文件 -function downloadFile(content, filename, mimeType = 'text/plain') { - const blob = new Blob([content], { type: mimeType }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -// 显示状态信息 -function showStatus(element, message, type = 'info') { - if (!element) return; - - element.classList.remove('hidden'); - element.textContent = message; - - // 移除所有状态类 - element.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600'); - element.classList.remove('border-green-500', 'bg-green-50', 'text-green-700'); - element.classList.remove('border-red-500', 'bg-red-50', 'text-red-700'); - element.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700'); - - // 根据类型添加相应的样式类 - switch (type) { - case 'success': - element.classList.add('border-green-500', 'bg-green-50', 'text-green-700'); - break; - case 'error': - element.classList.add('border-red-500', 'bg-red-50', 'text-red-700'); - break; - case 'loading': - element.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700'); - break; - default: - element.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600'); - } -} - -// 本地存储操作 -const storage = { - // 保存数据到本地存储 - set(key, value) { - try { - localStorage.setItem(key, JSON.stringify(value)); - return true; - } catch (error) { - console.error('保存到本地存储失败:', error); - return false; - } - }, - - // 从本地存储获取数据 - get(key, defaultValue = null) { - try { - const item = localStorage.getItem(key); - return item ? JSON.parse(item) : defaultValue; - } catch (error) { - console.error('从本地存储获取数据失败:', error); - return defaultValue; - } - }, - - // 删除本地存储中的数据 - remove(key) { - try { - localStorage.removeItem(key); - return true; - } catch (error) { - console.error('删除本地存储数据失败:', error); - return false; - } - } -}; - -// 防抖函数 -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -} - -// 节流函数 -function throttle(func, limit) { - let inThrottle; - return function() { - const args = arguments; - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; -} - -// 格式化日期时间 -function formatDateTime(date = new Date()) { - return date.toLocaleString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); -} - -// 深拷贝对象 -function deepClone(obj) { - if (obj === null || typeof obj !== 'object') return obj; - if (obj instanceof Date) return new Date(obj.getTime()); - if (obj instanceof Array) return obj.map(item => deepClone(item)); - if (typeof obj === 'object') { - const clonedObj = {}; - for (const key in obj) { - if (obj.hasOwnProperty(key)) { - clonedObj[key] = deepClone(obj[key]); - } - } - return clonedObj; - } -} - -// 检查对象是否为空 -function isEmpty(obj) { - if (obj == null) return true; - if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0; - if (typeof obj === 'object') return Object.keys(obj).length === 0; - return false; -} - -// 自动调整文本域高度 -function autoResizeTextarea(textarea) { - if (!textarea) return; - - // 重置高度以获取正确的scrollHeight - textarea.style.height = 'auto'; - - // 计算新高度,限制最大高度 - const newHeight = Math.min(textarea.scrollHeight, 120); // 最大120px(约5行) - textarea.style.height = newHeight + 'px'; -} - -// 流式文本处理 -class StreamProcessor { - constructor(onChunk, onComplete) { - this.onChunk = onChunk; - this.onComplete = onComplete; - this.buffer = ''; - } - - // 处理数据块 - processChunk(chunk) { - this.buffer += chunk; - - // 尝试解析完整的JSON行 - const lines = this.buffer.split('\n'); - this.buffer = lines.pop() || ''; // 保留不完整的行 - - for (const line of lines) { - if (line.trim()) { - try { - // 处理SSE格式 - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') { - this.onComplete(); - return; - } - - const parsed = JSON.parse(data); - this.onChunk(parsed); - } - } catch (error) { - console.warn('解析流数据失败:', error, line); - } - } - } - } -} - -// 创建流式请求 -async function createStreamRequest(url, options, onChunk, onComplete) { - const processor = new StreamProcessor(onChunk, onComplete); - - try { - const response = await fetch(url, { - ...options, - headers: { - ...options.headers, - 'Accept': 'text/event-stream', - 'Cache-Control': 'no-cache' - } - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - processor.processChunk(chunk); - } - } catch (error) { - throw error; - } -} - -// 导出工具函数 -window.Utils = { - escapeHtml, - scrollToBottom, - generateId, - parseSVGResponse, - downloadFile, - showStatus, - storage, - debounce, - throttle, - formatDateTime, - deepClone, - isEmpty, - autoResizeTextarea, - StreamProcessor, - createStreamRequest + +// 下载文件 +function downloadFile(content, filename, mimeType = 'text/plain') { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// 显示状态信息 +function showStatus(element, message, type = 'info') { + if (!element) return; + + element.classList.remove('hidden'); + element.textContent = message; + + // 移除所有状态类 + element.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600'); + element.classList.remove('border-green-500', 'bg-green-50', 'text-green-700'); + element.classList.remove('border-red-500', 'bg-red-50', 'text-red-700'); + element.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700'); + + // 根据类型添加相应的样式类 + switch (type) { + case 'success': + element.classList.add('border-green-500', 'bg-green-50', 'text-green-700'); + break; + case 'error': + element.classList.add('border-red-500', 'bg-red-50', 'text-red-700'); + break; + case 'loading': + element.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700'); + break; + default: + element.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600'); + } +} + +// 本地存储操作 +const storage = { + // 保存数据到本地存储 + set(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + console.error('保存到本地存储失败:', error); + return false; + } + }, + + // 从本地存储获取数据 + get(key, defaultValue = null) { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (error) { + console.error('从本地存储获取数据失败:', error); + return defaultValue; + } + }, + + // 删除本地存储中的数据 + remove(key) { + try { + localStorage.removeItem(key); + return true; + } catch (error) { + console.error('删除本地存储数据失败:', error); + return false; + } + } +}; + +// 防抖函数 +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// 节流函数 +function throttle(func, limit) { + let inThrottle; + return function() { + const args = arguments; + const context = this; + if (!inThrottle) { + func.apply(context, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} + +// 格式化日期时间 +function formatDateTime(date = new Date()) { + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +} + +// 深拷贝对象 +function deepClone(obj) { + if (obj === null || typeof obj !== 'object') return obj; + if (obj instanceof Date) return new Date(obj.getTime()); + if (obj instanceof Array) return obj.map(item => deepClone(item)); + if (typeof obj === 'object') { + const clonedObj = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + clonedObj[key] = deepClone(obj[key]); + } + } + return clonedObj; + } +} + +// 检查对象是否为空 +function isEmpty(obj) { + if (obj == null) return true; + if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0; + if (typeof obj === 'object') return Object.keys(obj).length === 0; + return false; +} + +// 自动调整文本域高度 +function autoResizeTextarea(textarea) { + if (!textarea) return; + + // 重置高度以获取正确的scrollHeight + textarea.style.height = 'auto'; + + // 计算新高度,限制最大高度 + const newHeight = Math.min(textarea.scrollHeight, 120); // 最大120px(约5行) + textarea.style.height = newHeight + 'px'; +} + +// 流式文本处理 +class StreamProcessor { + constructor(onChunk, onComplete) { + this.onChunk = onChunk; + this.onComplete = onComplete; + this.buffer = ''; + this.completed = false; + } + + complete(info = {}) { + if (this.completed) return; + this.completed = true; + if (typeof this.onComplete === 'function') { + this.onComplete(info); + } + } + + // 处理数据块 + processChunk(chunk) { + this.buffer += chunk; + + // 尝试解析完整的JSON行 + const lines = this.buffer.split('\n'); + this.buffer = lines.pop() || ''; // 保留不完整的行 + + for (const line of lines) { + if (line.trim()) { + try { + // 处理SSE格式 + if (line.startsWith('data: ')) { + const data = line.slice(6); + if (data === '[DONE]') { + this.complete({ aborted: false }); + return; + } + + const parsed = JSON.parse(data); + this.onChunk(parsed); + } + } catch (error) { + console.warn('解析流数据失败:', error, line); + } + } + } + } +} + +// 创建流式请求 +function createStreamRequest(url, options, onChunk, onComplete) { + const processor = new StreamProcessor(onChunk, onComplete); + const controller = new AbortController(); + + const fetchPromise = (async () => { + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + headers: { + ...options.headers, + 'Accept': 'text/event-stream', + 'Cache-Control': 'no-cache' + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + processor.processChunk(chunk); + if (processor.completed) { + break; + } + } + + if (!processor.completed) { + processor.complete({ aborted: false }); + } + } catch (error) { + if (error.name === 'AbortError') { + processor.complete({ aborted: true }); + return; + } + throw error; + } + })(); + + return { + cancel: () => controller.abort(), + finished: fetchPromise + }; +} + +// 导出工具函数 +window.Utils = { + escapeHtml, + scrollToBottom, + generateId, + parseSVGResponse, + downloadFile, + showStatus, + storage, + debounce, + throttle, + formatDateTime, + deepClone, + isEmpty, + autoResizeTextarea, + StreamProcessor, + createStreamRequest };