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
-
-\`\`\`
-包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
-
- `已经为您调整完成!
-\`\`\`svg
-
-\`\`\`
-采用了更加鲜明的色彩组合,希望您满意!`
- ];
-
- 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
+
+\`\`\`
+包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
+
+ `已经为您调整完成!
+\`\`\`svg
+
+\`\`\`
+采用了更加鲜明的色彩组合,希望您满意!`
+ ];
+
+ 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*')) {
- const svgEndIndex = fullContent.indexOf('') + 6; // +6 是 '' 的长度
-
- // 提取完整的SVG内容
- const svgStartMatch = fullContent.match(/```(?:svg)?\s*')) {
- 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*')) {
- 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*')) {
+ const svgEndIndex = fullContent.indexOf('') + 6;
+ const svgStartMatch = fullContent.match(/```(?:svg)?\s*')) {
+ 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*')) {
+ 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()}
+`;
- 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 = `
-
-
-
-
${Utils.escapeHtml(svgContent)}
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- // 关闭模态窗
- const closeModal = () => {
- document.body.removeChild(modal);
- };
-
- modal.querySelector('.close-modal').addEventListener('click', closeModal);
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- closeModal();
- }
- });
-
- // 复制代码
- modal.querySelector('.copy-btn').addEventListener('click', () => {
- navigator.clipboard.writeText(svgContent).then(() => {
- const btn = modal.querySelector('.copy-btn');
- const originalHTML = btn.innerHTML;
- btn.innerHTML = '
已复制';
- btn.classList.remove('bg-blue-500', 'hover:bg-blue-600');
- btn.classList.add('bg-green-500', 'hover:bg-green-600');
-
- setTimeout(() => {
- btn.innerHTML = originalHTML;
- btn.classList.remove('bg-green-500', 'hover:bg-green-600');
- btn.classList.add('bg-blue-500', 'hover:bg-blue-600');
- }, 2000);
- });
- });
- }
-
- // 打开API配置模态窗
- openConfigModal() {
- this.configModal.classList.add('active');
- const apiConfig = window.apiClient.getConfig();
- this.apiUrlInput.value = apiConfig.url || '';
- this.apiKeyInput.value = apiConfig.key || '';
- this.apiModelInput.value = apiConfig.model || '';
- }
-
- // 关闭API配置模态窗
- closeConfigModal() {
- this.configModal.classList.remove('active');
- }
-
- // 保存API配置
- saveAPIConfig() {
- const config = {
- url: this.apiUrlInput.value.trim(),
- key: this.apiKeyInput.value.trim(),
- model: this.apiModelInput.value.trim()
- };
-
- if (!config.url || !config.key || !config.model) {
- Utils.showStatus(this.configStatus, '⚠️ 请填写所有字段', 'error');
- return;
- }
-
- window.apiClient.saveConfig(config);
- Utils.showStatus(this.configStatus, '✅ 配置已保存成功!', 'success');
-
- setTimeout(() => {
- this.closeConfigModal();
- }, 1500);
- }
-
- // 测试API连接
- async testAPIConnection() {
- const config = {
- url: this.apiUrlInput.value.trim(),
- key: this.apiKeyInput.value.trim(),
- model: this.apiModelInput.value.trim()
- };
-
- if (!config.url || !config.key || !config.model) {
- Utils.showStatus(this.configStatus, '⚠️ 请先填写所有字段', 'error');
- return;
- }
-
- Utils.showStatus(this.configStatus, '🔄 正在测试连接...', 'loading');
-
- try {
- // 临时保存配置进行测试
- window.apiClient.saveConfig(config);
- await window.apiClient.testConnection();
- Utils.showStatus(this.configStatus, '✅ 连接测试成功!', 'success');
- } catch (error) {
- Utils.showStatus(this.configStatus, `❌ 连接失败: ${error.message}`, 'error');
- }
- }
-}
-
-// 页面加载完成后初始化应用
-document.addEventListener('DOMContentLoaded', () => {
- window.app = new ProductCanvasApp();
+
+ // 显示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 = `
+
+
+
+
${Utils.escapeHtml(svgContent)}
+
+
+
+
+
+ `;
+
+ document.body.appendChild(modal);
+
+ // 关闭模态窗
+ const closeModal = () => {
+ document.body.removeChild(modal);
+ };
+
+ modal.querySelector('.close-modal').addEventListener('click', closeModal);
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ closeModal();
+ }
+ });
+
+ // 复制代码
+ modal.querySelector('.copy-btn').addEventListener('click', () => {
+ navigator.clipboard.writeText(svgContent).then(() => {
+ const btn = modal.querySelector('.copy-btn');
+ const originalHTML = btn.innerHTML;
+ btn.innerHTML = '
已复制';
+ btn.classList.remove('bg-blue-500', 'hover:bg-blue-600');
+ btn.classList.add('bg-green-500', 'hover:bg-green-600');
+
+ setTimeout(() => {
+ btn.innerHTML = originalHTML;
+ btn.classList.remove('bg-green-500', 'hover:bg-green-600');
+ btn.classList.add('bg-blue-500', 'hover:bg-blue-600');
+ }, 2000);
+ });
+ });
+ }
+
+ // 打开API配置模态窗
+ openConfigModal() {
+ this.configModal.classList.add('active');
+ const apiConfig = window.apiClient.getConfig();
+ this.apiUrlInput.value = apiConfig.url || '';
+ this.apiKeyInput.value = apiConfig.key || '';
+ this.apiModelInput.value = apiConfig.model || '';
+ }
+
+ // 关闭API配置模态窗
+ closeConfigModal() {
+ this.configModal.classList.remove('active');
+ }
+
+ // 保存API配置
+ saveAPIConfig() {
+ const config = {
+ url: this.apiUrlInput.value.trim(),
+ key: this.apiKeyInput.value.trim(),
+ model: this.apiModelInput.value.trim()
+ };
+
+ if (!config.url || !config.key || !config.model) {
+ Utils.showStatus(this.configStatus, '⚠️ 请填写所有字段', 'error');
+ return;
+ }
+
+ window.apiClient.saveConfig(config);
+ Utils.showStatus(this.configStatus, '✅ 配置已保存成功!', 'success');
+
+ setTimeout(() => {
+ this.closeConfigModal();
+ }, 1500);
+ }
+
+ // 测试API连接
+ async testAPIConnection() {
+ const config = {
+ url: this.apiUrlInput.value.trim(),
+ key: this.apiKeyInput.value.trim(),
+ model: this.apiModelInput.value.trim()
+ };
+
+ if (!config.url || !config.key || !config.model) {
+ Utils.showStatus(this.configStatus, '⚠️ 请先填写所有字段', 'error');
+ return;
+ }
+
+ Utils.showStatus(this.configStatus, '🔄 正在测试连接...', 'loading');
+
+ try {
+ // 临时保存配置进行测试
+ window.apiClient.saveConfig(config);
+ await window.apiClient.testConnection();
+ Utils.showStatus(this.configStatus, '✅ 连接测试成功!', 'success');
+ } catch (error) {
+ Utils.showStatus(this.configStatus, `❌ 连接失败: ${error.message}`, 'error');
+ }
+ }
+}
+
+// 页面加载完成后初始化应用
+document.addEventListener('DOMContentLoaded', () => {
+ window.app = new ProductCanvasApp();
});
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
};