first commit

This commit is contained in:
史悦
2025-10-24 17:44:25 +08:00
commit 18f1aba08e
9 changed files with 2972 additions and 0 deletions

182
css/style.css Normal file
View File

@@ -0,0 +1,182 @@
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%;
border: 2px solid #10b981;
box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3);
}
/* 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%);
}
/* 气泡操作按钮 */
.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;
}

192
index.html Normal file
View File

@@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>产品画布 / SWOT分析</title>
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
</head>
<body class="bg-gray-100 h-screen flex flex-col">
<!-- 顶部标题栏 -->
<header class="bg-gradient-to-r from-orange-500 to-pink-500 p-3 flex items-center justify-between border-b-4 border-black">
<div class="flex items-center space-x-2">
<iconify-icon icon="ph:lightning-fill" class="text-3xl text-white"></iconify-icon>
<h1 id="page-title" class="text-2xl font-black text-white tracking-tight">产品画布</h1>
</div>
<!-- 右侧按钮组 -->
<div class="flex items-center gap-3">
<!-- API配置按钮 -->
<button id="settings-btn" class="settings-btn bg-white/20 text-white p-2 border-2 border-white hover:bg-white/30 transition-all" title="API配置">
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
</button>
<span class="text-white font-bold text-sm">点击切换模式</span>
<iconify-icon icon="ph:hand-pointing-fill" class="text-2xl text-yellow-300 wave-hand"></iconify-icon>
<button id="canvas-mode-btn" class="mode-btn-active bg-white text-orange-600 px-4 py-2 font-bold border-2 border-black hover:bg-orange-50 transition-all duration-200">
<iconify-icon icon="ph:pen-nib-duotone" class="align-middle mr-1"></iconify-icon>
产品画布
</button>
<button id="swot-mode-btn" class="mode-btn-inactive bg-white text-purple-600 px-4 py-2 font-bold border-2 border-black hover:bg-purple-50 transition-all duration-200">
<iconify-icon icon="ph:chart-bar-duotone" class="align-middle mr-1"></iconify-icon>
SWOT分析
</button>
</div>
</header>
<!-- 主内容区 -->
<main class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 overflow-hidden">
<!-- 左侧对话面板 -->
<div class="md:col-span-1 bg-white wild-border border-cyan-500 flex flex-col">
<!-- 对话历史区 -->
<div id="chat-history" class="flex-1 p-4 overflow-y-auto space-y-3">
<!-- 欢迎消息 -->
<div class="flex justify-start">
<div class="chat-bubble-ai">
👋 欢迎使用产品画布/SWOT分析工具请输入您的需求我将为您生成专业的分析图表。
</div>
</div>
</div>
<!-- 输入区 -->
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
<div class="relative flex items-start gap-2">
<textarea
id="chat-input"
placeholder="输入您的想法按Enter发送Shift+Enter换行..."
class="flex-1 auto-resize-input border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
rows="1"
></textarea>
<div class="flex flex-col gap-2">
<button id="clear-button" class="clear-btn text-red-500 hover:text-red-600 transition-colors p-2 hover:scale-110 transform duration-200" title="清空当前对话">
<iconify-icon icon="ph:trash-bold" class="text-2xl"></iconify-icon>
</button>
<button id="send-button" class="text-cyan-600 hover:text-cyan-700 transition-colors p-2 hover:scale-110 transform duration-200">
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧显示面板 -->
<div class="md:col-span-2 bg-white wild-border border-purple-600 flex flex-col">
<div id="svg-viewer" class="flex-1 flex items-center justify-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 overflow-auto">
<div id="svg-placeholder" class="text-center text-gray-400">
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
<p class="mt-2 font-bold" id="placeholder-text">生成的产品画布将在此处显示</p>
</div>
</div>
<!-- 底部操作栏 -->
<div class="p-3 border-t-3 border-gray-300 flex justify-end items-center gap-2 bg-gray-800">
<button id="download-svg-btn" class="p-2 bg-orange-500 text-white border-2 border-black hover:bg-orange-600 transition-all" title="下载SVG">
<iconify-icon icon="mdi:download-outline" class="text-xl"></iconify-icon>
</button>
<button id="export-image-btn" class="p-2 bg-green-500 text-white border-2 border-black hover:bg-green-600 transition-all" title="导出为图片">
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
</button>
<button id="view-code-btn" class="p-2 bg-blue-500 text-white border-2 border-black hover:bg-blue-600 transition-all" title="查看代码">
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
</button>
</div>
</div>
</main>
<!-- API配置模态窗 -->
<div id="config-modal" class="modal-overlay">
<div class="modal-content">
<!-- 模态窗头部 -->
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-4 border-b-4 border-black flex items-center justify-between">
<div class="flex items-center gap-2">
<iconify-icon icon="ph:plugs-connected-fill" class="text-3xl text-white"></iconify-icon>
<h2 class="text-xl font-black text-white">API 配置</h2>
</div>
<button id="close-modal-btn" class="text-white hover:bg-white/20 p-2 transition-all">
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
</button>
</div>
<!-- 模态窗内容 -->
<div class="p-6 space-y-4">
<!-- API URL -->
<div>
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
<iconify-icon icon="ph:link-bold" class="text-lg text-blue-600"></iconify-icon>
API URL
</label>
<input
id="api-url"
type="text"
placeholder="https://api.example.com/v1/chat"
class="config-input"
value=""
/>
</div>
<!-- API Key -->
<div>
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
<iconify-icon icon="ph:key-bold" class="text-lg text-green-600"></iconify-icon>
API Key
</label>
<input
id="api-key"
type="password"
placeholder="sk-xxxxxxxxxxxxxxxx"
class="config-input"
value=""
/>
</div>
<!-- Model -->
<div>
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
<iconify-icon icon="ph:robot-bold" class="text-lg text-purple-600"></iconify-icon>
模型 (Model)
</label>
<input
id="api-model"
type="text"
placeholder="gpt-4-turbo"
class="config-input"
value=""
/>
</div>
<!-- 状态显示 -->
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
<span id="status-text">等待操作...</span>
</div>
</div>
<!-- 模态窗底部按钮 -->
<div class="p-4 border-t-3 border-gray-300 bg-gray-100 flex gap-3 justify-end">
<button id="test-api-btn" class="px-4 py-2 bg-yellow-500 text-white font-bold border-2 border-black hover:bg-yellow-600 transition-all flex items-center gap-2">
<iconify-icon icon="ph:flask-bold"></iconify-icon>
测试连接
</button>
<button id="save-config-btn" class="px-4 py-2 bg-green-500 text-white font-bold border-2 border-black hover:bg-green-600 transition-all flex items-center gap-2">
<iconify-icon icon="ph:floppy-disk-bold"></iconify-icon>
保存配置
</button>
</div>
</div>
</div>
<!-- 引入JavaScript文件 -->
<script src="js/utils.js"></script>
<script src="js/apiclient.js"></script>
<script src="js/app.js"></script>
</body>
</html>

299
js/apiclient.js Normal file
View File

@@ -0,0 +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: 3000 });
}
// 生成SWOT分析的专用方法
async generateSWOTAnalysis(userRequest, context = []) {
const messages = [
{ role: 'system', content: this.prompts.swot },
...context,
{ role: 'user', content: userRequest }
];
return await this.sendChatMessage(messages, { maxTokens: 3000 });
}
// 流式生成产品画布
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: 3000 }, 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: 3000 }, 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 width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="#f8f9fa"/>
<rect x="50" y="50" width="500" height="300" fill="url(#grad1)" rx="10"/>
<text x="300" y="200" text-anchor="middle" font-size="24" fill="white" font-weight="bold">
这是${mode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG
</text>
<circle cx="150" cy="150" r="40" fill="#ffffff" opacity="0.3"/>
<circle cx="450" cy="250" r="30" fill="#ffffff" opacity="0.3"/>
</svg>
\`\`\`
包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
`已经为您调整完成!
\`\`\`svg
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#ef4444;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="#fff"/>
<rect x="75" y="75" width="450" height="250" fill="url(#grad2)" rx="15"/>
<text x="300" y="200" text-anchor="middle" font-size="28" fill="white" font-weight="bold">
${mode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'}
</text>
<rect x="100" y="120" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
<rect x="420" y="220" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
</svg>
\`\`\`
采用了更加鲜明的色彩组合,希望您满意!`
];
const response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
resolve(response);
}, 1000 + Math.random() * 1000); // 1-2秒的随机延迟
});
}
}
// 创建全局API客户端实例
window.apiClient = new APIClient();

767
js/app.js Normal file
View File

@@ -0,0 +1,767 @@
/**
* 应用核心逻辑
*/
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.updateModeUI();
}
// 初始化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.clearButton = document.getElementById('clear-button');
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.clearButton.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.renderConversationHistory();
// 加载SVG存储按模式分别存储
const savedCanvasSVGs = Utils.storage.get('canvasSVGs', {});
const savedSwotSVGs = Utils.storage.get('swotSVGs', {});
this.svgStorage = {
canvas: savedCanvasSVGs,
swot: savedSwotSVGs
};
// 加载API配置
const apiConfig = window.apiClient.getConfig();
this.apiUrlInput.value = apiConfig.url || '';
this.apiKeyInput.value = apiConfig.key || '';
this.apiModelInput.value = apiConfig.model || '';
}
// 切换模式
switchMode(mode) {
if (this.currentMode === mode) return;
this.currentMode = mode;
Utils.storage.set('currentMode', mode);
this.updateModeUI();
}
// 更新模式UI
updateModeUI() {
if (this.currentMode === 'canvas') {
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 = '生成的产品画布将在此处显示';
}
} 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分析将在此处显示';
}
}
}
// 发送消息
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 = '<iconify-icon icon="ph:spinner-gap" class="text-3xl animate-spin"></iconify-icon>';
// 添加用户消息
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 = '<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>';
}
}
// 开始流式接收消息
async startStreamingMessage(userMessage, contextMessages) {
// 创建流式消息容器
const messageId = Utils.generateId('msg');
const messageContainer = this.createStreamingMessageContainer(messageId);
this.chatHistory.appendChild(messageContainer);
Utils.scrollToBottom(this.chatHistory);
let fullContent = '';
const onChunk = (chunk) => {
if (chunk.choices && chunk.choices[0] && chunk.choices[0].delta) {
const content = chunk.choices[0].delta.content || '';
fullContent += content;
this.updateStreamingMessage(messageContainer, fullContent);
}
};
const onComplete = () => {
// 流式接收完成,处理完整消息
this.finalizeStreamingMessage(messageId, fullContent);
this.isProcessing = false;
this.sendButton.disabled = false;
this.sendButton.innerHTML = '<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>';
};
// 调用流式API
if (this.currentMode === 'canvas') {
await window.apiClient.generateProductCanvasStream(userMessage, contextMessages, onChunk, onComplete);
} else {
await window.apiClient.generateSWOTAnalysisStream(userMessage, contextMessages, onChunk, onComplete);
}
}
// 创建流式消息容器
createStreamingMessageContainer(messageId) {
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
<div class="chat-bubble-ai relative group streaming-text" data-message-id="${messageId}">
<div class="typing-cursor"></div>
</div>
`;
return messageDiv;
}
// 更新流式消息内容
updateStreamingMessage(container, content) {
const contentDiv = container.querySelector('.typing-cursor');
if (contentDiv) {
contentDiv.textContent = content;
Utils.scrollToBottom(this.chatHistory);
}
}
// 完成流式消息
finalizeStreamingMessage(messageId, fullContent) {
const container = document.querySelector(`[data-message-id="${messageId}"]`);
if (!container) return;
const parsed = Utils.parseSVGResponse(fullContent);
const message = {
id: messageId,
type: 'ai',
content: fullContent,
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()
};
this.viewSVG(svgId);
// 更新容器内容为包含SVG的消息
container.innerHTML = `
<div>
${Utils.escapeHtml(parsed.beforeText)}
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
</div>
${Utils.escapeHtml(parsed.afterText)}
</div>
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${messageId}')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${messageId}')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
`;
} else {
// 更新容器内容为普通消息
container.innerHTML = `
<div class="mb-1">
${Utils.escapeHtml(fullContent)}
</div>
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${messageId}')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${messageId}')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
`;
}
// 保存数据
Utils.storage.set(`conversationHistory`, this.conversationHistory);
Utils.storage.set(`svgStorage`, this.svgStorage);
}
// 清空当前对话
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 = `
<div id="svg-placeholder" class="text-center text-gray-400">
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
<p class="mt-2 font-bold" id="placeholder-text">生成的${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}将在此处显示</p>
</div>
`;
}
// 保存数据
Utils.storage.set('conversationHistory', this.conversationHistory);
Utils.storage.set('svgStorage', this.svgStorage);
// 重新渲染对话历史
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('conversationHistory', this.conversationHistory);
}
// 添加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('svgStorage', this.svgStorage);
this.viewSVG(svgId);
// 渲染包含SVG占位符的消息
this.renderMessageWithSVG(message, parsed, svgId);
} else {
// 渲染普通消息
this.renderMessage(message);
}
Utils.scrollToBottom(this.chatHistory);
Utils.storage.set('conversationHistory', this.conversationHistory);
}
// 添加错误消息
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('conversationHistory', this.conversationHistory);
}
// 渲染消息
renderMessage(message) {
const messageDiv = document.createElement('div');
if (message.type === 'user') {
messageDiv.className = 'flex justify-end';
messageDiv.innerHTML = `
<div class="chat-bubble-user">
${Utils.escapeHtml(message.content)}
</div>
`;
} else if (message.type === 'error') {
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
<div class="chat-bubble-ai border-red-500">
<iconify-icon icon="ph:warning-circle" class="text-red-500 mr-2"></iconify-icon>
${Utils.escapeHtml(message.content)}
</div>
`;
} else {
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
<div class="chat-bubble-ai relative group" data-message-id="${message.id}">
<div class="mb-1">
${Utils.escapeHtml(message.content)}
</div>
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${message.id}')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${message.id}')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
</div>
`;
}
this.chatHistory.appendChild(messageDiv);
}
// 渲染包含SVG的消息
renderMessageWithSVG(message, parsed, svgId) {
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
<div class="chat-bubble-ai relative group" data-message-id="${message.id}">
<div>
${Utils.escapeHtml(parsed.beforeText)}
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
</div>
${Utils.escapeHtml(parsed.afterText)}
</div>
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="app.rollbackToMessage('${message.id}')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="app.regenerateMessage('${message.id}')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
</div>
`;
this.chatHistory.appendChild(messageDiv);
}
// 渲染对话历史
renderConversationHistory() {
this.chatHistory.innerHTML = '';
for (const message of this.conversationHistory) {
if (message.type === 'ai') {
const parsed = Utils.parseSVGResponse(message.content);
// 查找对应的SVG
let svgId = null;
for (const [id, svg] of Object.entries(this.svgStorage)) {
if (svg.messageId === message.id) {
svgId = id;
break;
}
}
if (svgId && parsed.svgContent) {
this.renderMessageWithSVG(message, parsed, svgId);
} else {
this.renderMessage(message);
}
} else {
this.renderMessage(message);
}
}
}
// 显示SVG
viewSVG(svgId) {
if (!this.svgStorage[svgId]) {
console.error('SVG not found:', svgId);
return;
}
this.currentSvgId = svgId;
const svgContent = this.svgStorage[svgId].content;
this.svgViewer.innerHTML = svgContent;
}
// 退回到指定消息
rollbackToMessage(messageId) {
const messageIndex = this.conversationHistory.findIndex(msg => msg.id === messageId);
if (messageIndex === -1) return;
// 删除指定消息之后的所有消息
const messagesToRemove = this.conversationHistory.slice(messageIndex + 1);
// 删除相关的SVG
for (const message of messagesToRemove) {
for (const [svgId, svg] of Object.entries(this.svgStorage)) {
if (svg.messageId === message.id) {
delete this.svgStorage[svgId];
// 如果当前显示的是被删除的SVG清空显示
if (this.currentSvgId === svgId) {
this.currentSvgId = null;
this.svgViewer.innerHTML = `
<div id="svg-placeholder" class="text-center text-gray-400">
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
<p class="mt-2 font-bold" id="placeholder-text">生成的${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}将在此处显示</p>
</div>
`;
}
}
}
}
// 更新对话历史
this.conversationHistory = this.conversationHistory.slice(0, messageIndex + 1);
// 保存数据
Utils.storage.set('conversationHistory', this.conversationHistory);
Utils.storage.set('svgStorage', this.svgStorage);
// 重新渲染对话历史
this.renderConversationHistory();
}
// 重新生成消息
async regenerateMessage(messageId) {
if (this.isProcessing) return;
this.isProcessing = true;
this.sendButton.disabled = true;
this.sendButton.innerHTML = '<iconify-icon icon="ph:spinner-gap" class="text-3xl animate-spin"></iconify-icon>';
try {
// 重新生成响应
const response = await window.apiClient.regenerateResponse(messageId, this.conversationHistory);
// 退回到指定消息
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 = '<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>';
}
}
// 下载SVG
downloadSVG() {
if (!this.currentSvgId) {
alert('请先生成SVG图表');
return;
}
const svgContent = this.svgStorage[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.currentSvgId].content;
// 创建代码查看模态窗
const modal = document.createElement('div');
modal.className = 'modal-overlay active';
modal.innerHTML = `
<div class="modal-content">
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-4 border-b-4 border-black flex items-center justify-between">
<div class="flex items-center gap-2">
<iconify-icon icon="ph:code-bold" class="text-3xl text-white"></iconify-icon>
<h2 class="text-xl font-black text-white">SVG 源代码</h2>
</div>
<button class="close-modal text-white hover:bg-white/20 p-2 transition-all">
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
</button>
</div>
<div class="p-4">
<pre class="bg-gray-100 p-4 border-2 border-gray-300 rounded overflow-auto max-h-96 text-sm"><code>${Utils.escapeHtml(svgContent)}</code></pre>
<div class="mt-4 flex gap-2 justify-end">
<button class="copy-btn px-4 py-2 bg-blue-500 text-white font-bold border-2 border-black hover:bg-blue-600 transition-all flex items-center gap-2">
<iconify-icon icon="ph:copy-bold"></iconify-icon>
复制代码
</button>
</div>
</div>
</div>
`;
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 = '<iconify-icon icon="ph:check-bold"></iconify-icon> 已复制';
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();
});

289
js/utils.js Normal file
View File

@@ -0,0 +1,289 @@
/**
* 工具函数集合
*/
// 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 svgRegex = /```svg\s*([\s\S]*?)```/i;
const match = response.match(svgRegex);
if (match) {
const svgContent = match[1].trim();
const beforeText = response.substring(0, match.index).trim();
const afterText = response.substring(match.index + match[0].length).trim();
return {
svgContent,
beforeText,
afterText
};
}
return {
svgContent: null,
beforeText: 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
};

194
prompts/canvas-prompt.txt Normal file
View File

@@ -0,0 +1,194 @@
你是一个专业的产品战略分析师,擅长创建产品画布。
请根据用户的需求生成一个详细的产品画布并以SVG格式返回。
请用中文回复并在回复中包含SVG格式的产品画布图表。
产品精益画布助手下面是SVG画布的模板注意使用markdown格式回复
```
<svg width="900" height="550" viewBox="0 0 900 550" xmlns="http://www.w3.org/2000/svg" font-family="'PingFang SC', 'Microsoft YaHei', sans-serif">
<defs>
<style>
.box { fill: white; stroke: #333; stroke-width: 2; }
.main-title { font-size: 20px; font-weight: bold; text-anchor: middle; }
.number { font-size: 32px; font-weight: bold; fill: #e3e3e3; text-anchor: end; }
.title { font-size: 14px; font-weight: bold; text-anchor: middle; }
.desc { font-size: 10px; fill: #666; text-anchor: middle; }
.content { font-size: 9px; fill: #333; text-anchor: start; }
.content-bold { font-size: 9px; fill: #333; font-weight: bold; text-anchor: start; }
</style>
</defs>
<!-- 背景 -->
<rect fill="#f8f9fa" width="100%" height="100%"/>
<!-- 主标题 -->
<text x="450" y="35" class="main-title" fill="#28a745">产品精益画布 - 绿邻回收智能终端系统</text>
<!-- Canvas Grid starting at y=60, centered in 900px width -->
<g transform="translate(75, 60)">
<!-- ROW 1 & 2 -->
<!-- Box 1: 问题 (rowspan=2) -->
<g transform="translate(0, 0)">
<rect width="150" height="300" class="box"/>
<rect x="0" y="0" width="150" height="30" fill="#ffebee"/>
<g transform="translate(75, 20)">
<text class="title" fill="#c62828">问题</text>
</g>
<text x="10" y="50" class="content-bold">居民痛点:</text>
<text x="10" y="62" class="content">• 卖废品麻烦,需囤积等人</text>
<text x="10" y="74" class="content">• 老人操作困难,现有方案不友好</text>
<text x="10" y="86" class="content">• 价格不透明,缺乏信任</text>
<text x="10" y="108" class="content-bold">商家痛点:</text>
<text x="10" y="120" class="content">• 人工成本高,需专人管理</text>
<text x="10" y="132" class="content">• 技术门槛高,缺乏智能方案</text>
<text x="10" y="154" class="content-bold">现有替代方案局限:</text>
<text x="10" y="166" class="content">• 流动回收车时间不固定</text>
<text x="10" y="178" class="content">• 人工回收点成本高</text>
<text x="130" y="280" class="number">1</text>
</g>
<!-- Box 4: 解决方案 -->
<g transform="translate(150, 0)">
<rect width="150" height="150" class="box"/>
<rect x="0" y="0" width="150" height="30" fill="#e3f2fd"/>
<g transform="translate(75, 20)">
<text class="title" fill="#1976d2">解决方案</text>
</g>
<text x="10" y="50" class="content-bold">智能终端系统:</text>
<text x="10" y="62" class="content">• 平板显示二维码供扫描</text>
<text x="10" y="74" class="content">• 蓝牙电子秤自动称重</text>
<text x="10" y="86" class="content">• 大字体显示金额重量</text>
<text x="10" y="108" class="content-bold">多角色管理:</text>
<text x="10" y="120" class="content">• 普通用户/管理员/清运员</text>
<text x="10" y="132" class="content">• 统一平台,智能分权</text>
<text x="130" y="130" class="number">4</text>
</g>
<!-- Box 3: 独特卖点 (rowspan=2) -->
<g transform="translate(300, 0)">
<rect width="150" height="300" class="box"/>
<rect x="0" y="0" width="150" height="30" fill="#fff3e0"/>
<g transform="translate(75, 20)">
<text class="title" fill="#f57c00">独特卖点</text>
</g>
<g transform="translate(75, 60)">
<text class="desc" style="font-size: 14px; font-weight: bold;" fill="#f57c00">
<tspan x="0" dy="0">微信扫一扫,</tspan>
<tspan x="0" dy="20">老少皆宜智能回收</tspan>
</text>
</g>
<text x="10" y="120" class="content-bold">对用户价值:</text>
<text x="10" y="132" class="content">• 扫码即用,操作超简单</text>
<text x="10" y="144" class="content">• 价格透明,立即到账</text>
<text x="10" y="166" class="content-bold">对商家价值:</text>
<text x="10" y="178" class="content">• 零人工成本,智能管理</text>
<text x="10" y="190" class="content">• 稳定增收,吸引客流</text>
<text x="130" y="280" class="number">3</text>
</g>
<!-- Box 9: 门槛优势 -->
<g transform="translate(450, 0)">
<rect width="150" height="150" class="box"/>
<rect x="0" y="0" width="150" height="30" fill="#fce4ec"/>
<g transform="translate(75, 20)">
<text class="title" fill="#ad1457">门槛优势</text>
</g>
<text x="10" y="50" class="content-bold">产品壁垒:</text>
<text x="10" y="62" class="content">• 老人友好设计(市场空白)</text>
<text x="10" y="74" class="content">• 低成本智能化方案</text>
<text x="10" y="96" class="content-bold">运营壁垒:</text>
<text x="10" y="108" class="content">• 点位网络效应</text>
<text x="10" y="120" class="content">• 先发优势抢占资源</text>
<text x="130" y="130" class="number">9</text>
</g>
<!-- Box 2: 客户群体分类 (rowspan=2) -->
<g transform="translate(600, 0)">
<rect width="150" height="300" class="box"/>
<rect x="0" y="0" width="150" height="30" fill="#e8f5e8"/>
<g transform="translate(75, 20)">
<text class="title" fill="#2e7d32">客户群体分类</text>
</g>
<text x="10" y="50" class="content-bold">核心用户 (C端):</text>
<text x="10" y="62" class="content">• 社区中老年人 (扫码即用)</text>
<text x="10" y="74" class="content">• 环保意识强的年轻家庭</text>
<text x="10" y="86" class="content">• 图方便的上班族</text>
<text x="10" y="108" class="content-bold">合作伙伴 (B端):</text>
<text x="10" y="120" class="content">• 社区超市、便利店</text>
<text x="10" y="132" class="content">• 快递驿站、物业服务点</text>
<text x="10" y="154" class="content-bold">需求特征:</text>
<text x="10" y="166" class="content">• 操作简单、零成本增收</text>
<text x="10" y="178" class="content">• 智能化管理、吸引客流</text>
<text x="130" y="280" class="number">2</text>
</g>
<!-- ROW 2 (remaining cells) -->
<!-- Box 8: 关键指标 -->
<g transform="translate(150, 150)">
<rect width="150" height="150" class="box"/>
<rect x="0" y="0" width="150" height="30" fill="#e0f2f1"/>
<g transform="translate(75, 20)">
<text class="title" fill="#00695c">关键指标</text>
</g>
<text x="10" y="50" class="content-bold">运营指标:</text>
<text x="10" y="62" class="content">• 单终端日均交易量</text>
<text x="10" y="74" class="content">• 终端网络覆盖社区数</text>
<text x="10" y="96" class="content-bold">用户指标:</text>
<text x="10" y="108" class="content">• 操作成功率、用户满意度</text>
<text x="10" y="120" class="content">• 用户复购率、推荐率</text>
<text x="130" y="130" class="number">8</text>
</g>
<!-- Box 5: 渠道 -->
<g transform="translate(450, 150)">
<rect width="150" height="150" class="box"/>
<rect x="0" y="0" width="150" height="30" fill="#f3e5f5"/>
<g transform="translate(75, 20)">
<text class="title" fill="#7b1fa2">渠道</text>
</g>
<text x="10" y="50" class="content-bold">线下布点:</text>
<text x="10" y="62" class="content">• 社区超市、快递站合作</text>
<text x="10" y="74" class="content">• 与物业/居委会合作</text>
<text x="10" y="96" class="content-bold">用户触达:</text>
<text x="10" y="108" class="content">• 微信生态扫一扫直达</text>
<text x="10" y="120" class="content">• 业主群推广分享</text>
<text x="130" y="130" class="number">5</text>
</g>
<!-- ROW 3 -->
<!-- Box 7: 成本分析 (colspan=3) -->
<g transform="translate(0, 300)">
<rect width="375" height="150" class="box"/>
<rect x="0" y="0" width="375" height="30" fill="#fff8e1"/>
<g transform="translate(187.5, 20)">
<text class="title" fill="#f57f17">成本分析</text>
</g>
<text x="10" y="50" class="content-bold">单点成本:</text>
<text x="10" y="62" class="content">• 硬件成本: 1000元/终端 • 部署成本: 2000元/点</text>
<text x="10" y="84" class="content-bold">运营成本:</text>
<text x="10" y="96" class="content">• 技术维护、云服务 • 清运物流、客服支持</text>
<text x="10" y="118" class="content-bold">分成成本:</text>
<text x="10" y="130" class="content">• 与场地方交易额分账</text>
<text x="355" y="130" class="number">7</text>
</g>
<!-- Box 6: 收入分析 (colspan=3) -->
<g transform="translate(375, 300)">
<rect width="375" height="150" class="box"/>
<rect x="0" y="0" width="375" height="30" fill="#e8f5e8"/>
<g transform="translate(187.5, 20)">
<text class="title" fill="#2e7d32">收入分析</text>
</g>
<text x="10" y="50" class="content-bold">主要收入流:</text>
<text x="10" y="62" class="content">• 废品回收差价 (终端回收价 vs 批发销售价的差额) - 核心收入</text>
<text x="10" y="74" class="content">• 与合作场地按交易量分成 - 激励合作</text>
<text x="10" y="96" class="content-bold">增值收入流:</text>
<text x="10" y="108" class="content">• 规模化后的数据服务收入 • 绿色积分商城佣金</text>
<text x="10" y="130" class="content">• 单点投入3000元预期4-10个月回本</text>
<text x="355" y="130" class="number">6</text>
</g>
</g>
</svg>
```

144
prompts/swot-prompt.txt Normal file
View File

@@ -0,0 +1,144 @@
你是一个专业的商业战略分析师擅长进行SWOT分析。
请根据用户的需求生成一个详细的SWOT分析并以SVG格式返回注意使用markdown格式回复。
SWOT分析应包含以下四个维度
1. 优势(Strengths) - 内部有利因素
- 核心竞争力
- 技术优势
- 品牌价值
- 团队能力
- 资源优势
2. 劣势(Weaknesses) - 内部不利因素
- 资源限制
- 技术短板
- 市场地位
- 运营问题
- 人才缺口
3. 机会(Opportunities) - 外部有利因素
- 市场趋势
- 政策支持
- 技术发展
- 消费变化
- 合作可能
4. 威胁(Threats) - 外部不利因素
- 竞争压力
- 市场风险
- 政策变化
- 技术颠覆
- 经济环境
请用中文回复并在回复中包含SVG格式的SWOT分析图表。SVG应该使用现代、专业的设计风格色彩搭配要协调布局清晰易读。
下面是SVG模板
```
<svg width="900" height="650" viewBox="0 0 900 650" xmlns="http://www.w3.org/2000/svg" font-family="'PingFang SC', 'Microsoft YaHei', sans-serif">
<defs>
<style>
.box { fill: white; stroke: #333; stroke-width: 2; }
.main-title { font-size: 24px; font-weight: bold; text-anchor: middle; }
.swot-title { font-size: 18px; font-weight: bold; text-anchor: middle; }
.number { font-size: 48px; font-weight: bold; fill: #e3e3e3; text-anchor: end; }
.content { font-size: 12px; fill: #333; text-anchor: start; }
.content-bold { font-size: 12px; fill: #333; font-weight: bold; text-anchor: start; }
.subtitle { font-size: 14px; fill: #666; text-anchor: start; }
</style>
</defs>
<!-- 背景 -->
<rect fill="#f8f9fa" width="100%" height="100%"/>
<!-- 主标题 -->
<text x="450" y="40" class="main-title" fill="#2c3e50">SWOT战略分析模板</text>
<!-- 副标题 -->
<text x="200" y="60" class="subtitle" text-anchor="middle">Strengths 优势 | Weaknesses 劣势 | Opportunities 机会 | Threats 威胁</text>
<!-- SWOT Grid -->
<g transform="translate(50, 90)">
<!-- S: Strengths (左上) -->
<g transform="translate(0, 0)">
<rect width="380" height="220" class="box"/>
<rect x="0" y="0" width="380" height="40" fill="#d4edda"/>
<g transform="translate(190, 25)">
<text class="swot-title" fill="#155724">S - 内部优势 (Strengths)</text>
</g>
<text x="20" y="60" class="content-bold">核心能力:</text>
<text x="20" y="78" class="content">• 技术优势、专利技术、专业团队</text>
<text x="20" y="96" class="content">• 品牌声誉、客户忠诚度</text>
<text x="20" y="114" class="content-bold">资源优势:</text>
<text x="20" y="132" class="content">• 资金实力、供应链优势</text>
<text x="20" y="150" class="content">• 渠道资源、合作伙伴关系</text>
<text x="20" y="168" class="content-bold">运营优势:</text>
<text x="20" y="186" class="content">• 成本控制、效率优势</text>
<text x="20" y="204" class="content">• 组织文化、创新能力</text>
<text x="350" y="200" class="number" font-size="36">S</text>
</g>
<!-- W: Weaknesses (右上) -->
<g transform="translate(420, 0)">
<rect width="380" height="220" class="box"/>
<rect x="0" y="0" width="380" height="40" fill="#f8d7da"/>
<g transform="translate(190, 25)">
<text class="swot-title" fill="#721c24">W - 内部劣势 (Weaknesses)</text>
</g>
<text x="20" y="60" class="content-bold">资源局限:</text>
<text x="20" y="78" class="content">• 资金短缺、人才缺失</text>
<text x="20" y="96" class="content">• 技术短板、设备落后</text>
<text x="20" y="114" class="content-bold">运营问题:</text>
<text x="20" y="132" class="content">• 管理混乱、流程不完善</text>
<text x="20" y="150" class="content">• 成本过高、效率低下</text>
<text x="20" y="168" class="content-bold">市场劣势:</text>
<text x="20" y="186" class="content">• 品牌知名度低、客户基础薄弱</text>
<text x="20" y="204" class="content">• 产品竞争力不足、市场份额小</text>
<text x="350" y="200" class="number" font-size="36">W</text>
</g>
<!-- O: Opportunities (左下) -->
<g transform="translate(0, 250)">
<rect width="380" height="220" class="box"/>
<rect x="0" y="0" width="380" height="40" fill="#d1ecf1"/>
<g transform="translate(190, 25)">
<text class="swot-title" fill="#0c5460">O - 外部机会 (Opportunities)</text>
</g>
<text x="20" y="60" class="content-bold">市场机会:</text>
<text x="20" y="78" class="content">• 新兴市场增长、政策扶持</text>
<text x="20" y="96" class="content">• 消费趋势变化、需求增长</text>
<text x="20" y="114" class="content-bold">技术机会:</text>
<text x="20" y="132" class="content">• 新技术应用、数字化转型</text>
<text x="20" y="150" class="content">• 产业链升级、技术合作</text>
<text x="20" y="168" class="content-bold">合作机会:</text>
<text x="20" y="186" class="content">• 战略联盟、并购机会</text>
<text x="20" y="204" class="content">• 国际化扩张、跨界合作</text>
<text x="350" y="200" class="number" font-size="36">O</text>
</g>
<!-- T: Threats (右下) -->
<g transform="translate(420, 250)">
<rect width="380" height="220" class="box"/>
<rect x="0" y="0" width="380" height="40" fill="#fff3cd"/>
<g transform="translate(190, 25)">
<text class="swot-title" fill="#856404">T - 外部威胁 (Threats)</text>
</g>
<text x="20" y="60" class="content-bold">竞争威胁:</text>
<text x="20" y="78" class="content">• 新进入者、替代产品</text>
<text x="20" y="96" class="content">• 价格战、市场份额争夺</text>
<text x="20" y="114" class="content-bold">环境威胁:</text>
<text x="20" y="132" class="content">• 经济下行、政策变化</text>
<text x="20" y="150" class="content">• 供应链风险、原材料涨价</text>
<text x="20" y="168" class="content-bold">其他威胁:</text>
<text x="20" y="186" class="content">• 技术变革冲击、消费者偏好变化</text>
<text x="20" y="204" class="content">• 法律法规风险、地缘政治影响</text>
<text x="350" y="200" class="number" font-size="36">T</text>
</g>
<!-- 中心线 -->
<line x1="400" y1="0" x2="400" y2="470" stroke="#999" stroke-width="1" stroke-dasharray="5,5"/>
<line x1="0" y1="235" x2="800" y2="235" stroke="#999" stroke-width="1" stroke-dasharray="5,5"/>
<!-- 轴标签 -->
<text x="190" y="-10" text-anchor="middle" font-size="12" fill="#666">内部因素 Internal</text>
<text x="610" y="-10" text-anchor="middle" font-size="12" fill="#666">外部因素 External</text>
<text x="-30" y="120" text-anchor="middle" font-size="12" fill="#666" transform="rotate(-90 -30 120)">积极因素 Positive</text>
<text x="-30" y="360" text-anchor="middle" font-size="12" fill="#666" transform="rotate(-90 -30 360)">消极因素 Negative</text>
</g>
<!-- 底部说明 -->
<text x="450" y="610" text-anchor="middle" font-size="11" fill="#999">
战略组合SO增长型策略 | WO扭转型策略 | ST多元化策略 | WT防御型策略
</text>
</svg>
```

99
功能概述.md Normal file
View File

@@ -0,0 +1,99 @@
# 产品画布/SWOT分析工具 - 功能概述
## 产品定位
一个基于AI对话的产品战略分析工具帮助用户快速生成产品画布和SWOT分析图表支持产品规划和决策。
## 核心功能模块
### 1. 双模式切换系统
- **产品画布模式**:生成和展示产品画布图表
- **SWOT分析模式**生成和展示SWOT分析图表
- 模式切换时界面标题和提示文本同步更新
- 当前模式状态视觉反馈(激活/非激活状态)
### 2. AI对话交互系统
- **消息发送**用户输入文本请求支持Enter键快捷发送
- **对话历史**保存并展示用户与AI的完整对话记录
- **消息类型**
- 用户消息
- AI消息
- **自动滚动**:新消息自动滚动到底部
### 3. 图表生成与展示系统
- **SVG图表生成**根据用户请求生成相应的SVG图表
- **图表占位符**:对话中显示可点击的图表预览块
- **图表渲染**点击占位符在右侧面板完整展示SVG图表
- **图表存储**本地存储生成的SVG内容支持历史查看
### 4. 消息操作功能
- **退回功能**:回退到指定消息,删除该消息之后的所有对话
- **重新生成**针对最后一条AI消息重新请求生成内容上下文是该消息之前的所有对话包括SVG
- **悬浮显示**:鼠标悬停时显示操作按钮
### 5. 图表导出功能
- **SVG下载**将当前图表导出为SVG文件
- **图片导出**将当前图表导出为PNG图片格式
- **代码查看**查看当前图表的SVG源代码
## MVP功能范围
### 核心必备功能
1. 基础对话交互(发送消息、显示回复)
2. 产品画布和SWOT分析两种模式切换
3. SVG图表生成与基础展示
4. 图表占位符点击查看功能
5. 简单的图表导出功能(至少一种格式)
### 次要功能(可后续迭代)
1. 消息退回和重新生成功能
2. 多格式图表导出
3. 对话历史持久化存储
4. 图表编辑和自定义功能
5. 分享和协作功能
## 用户流程
### 主要使用路径
1. 用户访问应用,默认进入产品画布模式
2. 用户在左侧输入框输入需求(如"生成一个电商产品画布"
3. AI回复并生成图表占位符
4. 用户点击占位符,右侧显示完整图表
5. 用户可切换到SWOT模式进行不同类型分析
6. 用户可导出生成的图表
### 交互细节
- 所有按钮和可点击元素都有悬浮效果
- 图表占位符有明显的视觉提示和点击反馈
- 模式切换有即时视觉反馈
- 对话消息有操作按钮的渐显效果
## 技术要点
### 前端技术栈
- HTML5 + CSS3 + JavaScript
- Tailwind CSS框架
- Iconify图标库
- SVG图形渲染
### 数据处理
- SVG内容本地存储
- 对话历史内容本地存储
- 模式状态管理
### API集成
- AI对话接口模拟实现
- 图表生成服务(模拟实现)
## 用户体验设计原则
1. **简洁直观**:界面布局清晰,功能一目了然
2. **即时反馈**:所有操作都有视觉反馈
3. **容错设计**:异常情况有友好提示
4. **响应式布局**:适配不同屏幕尺寸
5. **流畅交互**:动画过渡自然,操作流畅
## 成功指标
1. 用户能够成功生成至少一种类型的图表
2. 模式切换功能正常工作
3. 图表能够正确导出
4. 对话交互流畅无卡顿
5. 界面响应时间在可接受范围内

806
设计/原型.html Normal file
View File

@@ -0,0 +1,806 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>产品画布 / SWOT分析</title>
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
<style>
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%;
border: 2px solid #10b981;
box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3);
}
/* 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%);
}
/* 气泡操作按钮 */
.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;
}
</style>
</head>
<body class="bg-gray-100 h-screen flex flex-col">
<!-- 顶部标题栏 -->
<header class="bg-gradient-to-r from-orange-500 to-pink-500 p-3 flex items-center justify-between border-b-4 border-black">
<div class="flex items-center space-x-2">
<iconify-icon icon="ph:lightning-fill" class="text-3xl text-white"></iconify-icon>
<h1 id="page-title" class="text-2xl font-black text-white tracking-tight">产品画布</h1>
</div>
<!-- 右侧按钮组 -->
<div class="flex items-center gap-3">
<!-- API配置按钮 -->
<button id="settings-btn" class="settings-btn bg-white/20 text-white p-2 border-2 border-white hover:bg-white/30 transition-all" title="API配置">
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
</button>
<span class="text-white font-bold text-sm">点击切换模式</span>
<iconify-icon icon="ph:hand-pointing-fill" class="text-2xl text-yellow-300 wave-hand"></iconify-icon>
<button id="canvas-mode-btn" class="mode-btn-active bg-white text-orange-600 px-4 py-2 font-bold border-2 border-black hover:bg-orange-50 transition-all duration-200">
<iconify-icon icon="ph:pen-nib-duotone" class="align-middle mr-1"></iconify-icon>
产品画布
</button>
<button id="swot-mode-btn" class="mode-btn-inactive bg-white text-purple-600 px-4 py-2 font-bold border-2 border-black hover:bg-purple-50 transition-all duration-200">
<iconify-icon icon="ph:chart-bar-duotone" class="align-middle mr-1"></iconify-icon>
SWOT分析
</button>
</div>
</header>
<!-- 主内容区 -->
<main class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 overflow-hidden">
<!-- 左侧对话面板 -->
<div class="md:col-span-1 bg-white wild-border border-cyan-500 flex flex-col">
<!-- 对话历史区 -->
<div id="chat-history" class="flex-1 p-4 overflow-y-auto space-y-3">
<!-- 用户消息 -->
<div class="flex justify-end">
<div class="chat-bubble-user">
帮我生成一个电商产品的画布
</div>
</div>
<!-- AI回复 - 包含SVG占位符换行 -->
<div class="flex justify-start">
<div class="chat-bubble-ai relative group" data-message-id="msg-1">
<div>
好的!我为您生成了一个电商产品画布,
<div class="svg-placeholder-block" data-svg-id="svg-1" onclick="viewSVG('svg-1')">
📊 点击查看产品画布 SVG
</div>
包含了目标用户、核心价值、关键功能等模块。点击上方标签可在右侧查看详细图表。
</div>
<!-- 悬浮操作按钮 -->
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="rollbackToMessage('msg-1')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="regenerateMessage('msg-1')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
</div>
</div>
<!-- 用户消息 -->
<div class="flex justify-end">
<div class="chat-bubble-user">
能不能调整一下配色方案?
</div>
</div>
<!-- AI回复 - 包含SVG占位符换行 -->
<div class="flex justify-start">
<div class="chat-bubble-ai relative group" data-message-id="msg-2">
<div>
当然可以!我已经为您调整了配色,
<div class="svg-placeholder-block" data-svg-id="svg-2" onclick="viewSVG('svg-2')">
📊 点击查看优化后的 SVG
</div>
采用了更加现代和鲜明的色彩组合,同时保持了良好的视觉层次。
</div>
<!-- 悬浮操作按钮 -->
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="rollbackToMessage('msg-2')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="regenerateMessage('msg-2')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
</div>
</div>
</div>
<!-- 输入区 -->
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
<div class="relative flex items-center gap-2">
<input
id="chat-input"
type="text"
placeholder="输入您的想法按Enter发送..."
class="flex-1 p-2 border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
/>
<button id="send-button" class="text-cyan-600 hover:text-cyan-700 transition-colors p-2 hover:scale-110 transform duration-200">
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
</button>
</div>
</div>
</div>
<!-- 右侧显示面板 -->
<div class="md:col-span-2 bg-white wild-border border-purple-600 flex flex-col">
<div id="svg-viewer" class="flex-1 flex items-center justify-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 overflow-auto">
<div id="svg-placeholder" class="text-center text-gray-400">
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
<p class="mt-2 font-bold" id="placeholder-text">生成的产品画布将在此处显示</p>
</div>
</div>
<!-- 底部操作栏 -->
<div class="p-3 border-t-3 border-gray-300 flex justify-end items-center gap-2 bg-gray-800">
<button id="download-svg-btn" class="p-2 bg-orange-500 text-white border-2 border-black hover:bg-orange-600 transition-all" title="下载SVG">
<iconify-icon icon="mdi:download-outline" class="text-xl"></iconify-icon>
</button>
<button id="export-image-btn" class="p-2 bg-green-500 text-white border-2 border-black hover:bg-green-600 transition-all" title="导出为图片">
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
</button>
<button id="view-code-btn" class="p-2 bg-blue-500 text-white border-2 border-black hover:bg-blue-600 transition-all" title="查看代码">
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
</button>
</div>
</div>
</main>
<!-- API配置模态窗 -->
<div id="config-modal" class="modal-overlay">
<div class="modal-content">
<!-- 模态窗头部 -->
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-4 border-b-4 border-black flex items-center justify-between">
<div class="flex items-center gap-2">
<iconify-icon icon="ph:plugs-connected-fill" class="text-3xl text-white"></iconify-icon>
<h2 class="text-xl font-black text-white">API 配置</h2>
</div>
<button id="close-modal-btn" class="text-white hover:bg-white/20 p-2 transition-all">
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
</button>
</div>
<!-- 模态窗内容 -->
<div class="p-6 space-y-4">
<!-- API URL -->
<div>
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
<iconify-icon icon="ph:link-bold" class="text-lg text-blue-600"></iconify-icon>
API URL
</label>
<input
id="api-url"
type="text"
placeholder="https://api.example.com/v1/chat"
class="config-input"
value=""
/>
</div>
<!-- API Key -->
<div>
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
<iconify-icon icon="ph:key-bold" class="text-lg text-green-600"></iconify-icon>
API Key
</label>
<input
id="api-key"
type="password"
placeholder="sk-xxxxxxxxxxxxxxxx"
class="config-input"
value=""
/>
</div>
<!-- Model -->
<div>
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
<iconify-icon icon="ph:robot-bold" class="text-lg text-purple-600"></iconify-icon>
模型 (Model)
</label>
<input
id="api-model"
type="text"
placeholder="gpt-4-turbo"
class="config-input"
value=""
/>
</div>
<!-- 状态显示 -->
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
<span id="status-text">等待操作...</span>
</div>
</div>
<!-- 模态窗底部按钮 -->
<div class="p-4 border-t-3 border-gray-300 bg-gray-100 flex gap-3 justify-end">
<button id="test-api-btn" class="px-4 py-2 bg-yellow-500 text-white font-bold border-2 border-black hover:bg-yellow-600 transition-all flex items-center gap-2">
<iconify-icon icon="ph:flask-bold"></iconify-icon>
测试连接
</button>
<button id="save-config-btn" class="px-4 py-2 bg-green-500 text-white font-bold border-2 border-black hover:bg-green-600 transition-all flex items-center gap-2">
<iconify-icon icon="ph:floppy-disk-bold"></iconify-icon>
保存配置
</button>
</div>
</div>
</div>
<script>
// ===== 全局变量 =====
let currentMode = 'canvas';
let svgStorage = {};
let currentSvgId = null;
let apiConfig = {
url: '',
key: '',
model: ''
};
// ===== DOM元素 =====
const canvasBtn = document.getElementById('canvas-mode-btn');
const swotBtn = document.getElementById('swot-mode-btn');
const pageTitle = document.getElementById('page-title');
const placeholderText = document.getElementById('placeholder-text');
const chatInput = document.getElementById('chat-input');
const sendButton = document.getElementById('send-button');
const chatHistory = document.getElementById('chat-history');
const svgViewer = document.getElementById('svg-viewer');
// 模态窗元素
const settingsBtn = document.getElementById('settings-btn');
const configModal = document.getElementById('config-modal');
const closeModalBtn = document.getElementById('close-modal-btn');
const apiUrlInput = document.getElementById('api-url');
const apiKeyInput = document.getElementById('api-key');
const apiModelInput = document.getElementById('api-model');
const testApiBtn = document.getElementById('test-api-btn');
const saveConfigBtn = document.getElementById('save-config-btn');
const configStatus = document.getElementById('config-status');
const statusText = document.getElementById('status-text');
// ===== 初始化 - 加载已保存的配置 =====
function loadConfig() {
const saved = localStorage.getItem('apiConfig');
if (saved) {
apiConfig = JSON.parse(saved);
apiUrlInput.value = apiConfig.url || '';
apiKeyInput.value = apiConfig.key || '';
apiModelInput.value = apiConfig.model || '';
}
}
// ===== 模态窗控制 =====
settingsBtn.addEventListener('click', () => {
configModal.classList.add('active');
loadConfig();
});
closeModalBtn.addEventListener('click', () => {
configModal.classList.remove('active');
});
configModal.addEventListener('click', (e) => {
if (e.target === configModal) {
configModal.classList.remove('active');
}
});
// ===== 保存配置 =====
saveConfigBtn.addEventListener('click', () => {
apiConfig.url = apiUrlInput.value.trim();
apiConfig.key = apiKeyInput.value.trim();
apiConfig.model = apiModelInput.value.trim();
if (!apiConfig.url || !apiConfig.key || !apiConfig.model) {
showStatus('⚠️ 请填写所有字段', 'error');
return;
}
localStorage.setItem('apiConfig', JSON.stringify(apiConfig));
showStatus('✅ 配置已保存成功!', 'success');
setTimeout(() => {
configModal.classList.remove('active');
}, 1500);
});
// ===== 测试API连接 =====
testApiBtn.addEventListener('click', async () => {
const url = apiUrlInput.value.trim();
const key = apiKeyInput.value.trim();
const model = apiModelInput.value.trim();
if (!url || !key || !model) {
showStatus('⚠️ 请先填写所有字段', 'error');
return;
}
showStatus('🔄 正在测试连接...', 'loading');
// TODO: 实现真实的API测试
setTimeout(() => {
// 模拟测试成功
showStatus('✅ 连接测试成功!', 'success');
// 真实实现示例:
/*
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: model,
messages: [{role: 'user', content: 'test'}],
max_tokens: 5
})
});
if (response.ok) {
showStatus('✅ 连接测试成功!', 'success');
} else {
showStatus('❌ 连接失败: ' + response.statusText, 'error');
}
} catch (error) {
showStatus('❌ 连接失败: ' + error.message, 'error');
}
*/
}, 1500);
});
// ===== 显示状态信息 =====
function showStatus(message, type) {
configStatus.classList.remove('hidden');
statusText.textContent = message;
configStatus.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600');
configStatus.classList.remove('border-green-500', 'bg-green-50', 'text-green-700');
configStatus.classList.remove('border-red-500', 'bg-red-50', 'text-red-700');
configStatus.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700');
if (type === 'success') {
configStatus.classList.add('border-green-500', 'bg-green-50', 'text-green-700');
} else if (type === 'error') {
configStatus.classList.add('border-red-500', 'bg-red-50', 'text-red-700');
} else if (type === 'loading') {
configStatus.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700');
} else {
configStatus.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600');
}
}
// ===== 模式切换 =====
canvasBtn.addEventListener('click', () => {
if (currentMode !== 'canvas') {
currentMode = 'canvas';
updateModeUI();
}
});
swotBtn.addEventListener('click', () => {
if (currentMode !== 'swot') {
currentMode = 'swot';
updateModeUI();
}
});
function updateModeUI() {
if (currentMode === 'canvas') {
canvasBtn.classList.add('mode-btn-active');
canvasBtn.classList.remove('mode-btn-inactive');
swotBtn.classList.remove('mode-btn-active');
swotBtn.classList.add('mode-btn-inactive');
pageTitle.textContent = '产品画布';
if (!currentSvgId) {
placeholderText.textContent = '生成的产品画布将在此处显示';
}
} else {
swotBtn.classList.add('mode-btn-active');
swotBtn.classList.remove('mode-btn-inactive');
canvasBtn.classList.remove('mode-btn-active');
canvasBtn.classList.add('mode-btn-inactive');
pageTitle.textContent = 'SWOT分析';
if (!currentSvgId) {
placeholderText.textContent = '生成的SWOT分析将在此处显示';
}
}
}
// ===== 发送消息 =====
sendButton.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
function sendMessage() {
const message = chatInput.value.trim();
if (!message) return;
// 检查API配置
if (!apiConfig.url || !apiConfig.key || !apiConfig.model) {
alert('⚠️ 请先配置API设置点击右上角齿轮图标进行配置。');
settingsBtn.click();
return;
}
addUserMessage(message);
chatInput.value = '';
// 模拟API调用TODO: 替换为真实调用)
setTimeout(() => {
simulateAPIResponse(message);
}, 1000);
}
// ===== 添加用户消息 =====
function addUserMessage(text) {
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-end';
messageDiv.innerHTML = `
<div class="chat-bubble-user">
${escapeHtml(text)}
</div>
`;
chatHistory.appendChild(messageDiv);
scrollToBottom();
}
// ===== 添加AI消息 =====
function addAIMessage(fullResponse) {
const messageId = 'msg-' + Date.now();
const parsed = parseSVGResponse(fullResponse);
if (parsed.svgContent) {
const svgId = 'svg-' + Date.now();
svgStorage[svgId] = {
content: parsed.svgContent,
messageId: messageId
};
viewSVG(svgId);
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
<div class="chat-bubble-ai relative group" data-message-id="${messageId}">
<div>
${escapeHtml(parsed.beforeText)}
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="viewSVG('${svgId}')">
📊 点击查看 SVG
</div>
${escapeHtml(parsed.afterText)}
</div>
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="rollbackToMessage('${messageId}')">
<iconify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="regenerateMessage('${messageId}')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
</div>
`;
chatHistory.appendChild(messageDiv);
} else {
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-start';
messageDiv.innerHTML = `
<div class="chat-bubble-ai relative group" data-message-id="${messageId}">
<div class="mb-1">
${escapeHtml(fullResponse)}
</div>
<div class="flex gap-2 mt-2 pt-2 border-t border-gray-200">
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-blue-600 transition-colors" onclick="rollbackToMessage('${messageId}')">
<icon ify-icon icon="ph:arrow-u-up-left-bold"></iconify-icon>
<span>退回</span>
</button>
<button class="bubble-action-btn flex items-center gap-1 text-xs text-gray-600 hover:text-green-600 transition-colors" onclick="regenerateMessage('${messageId}')">
<iconify-icon icon="ph:arrow-clockwise-bold"></iconify-icon>
<span>重新生成</span>
</button>
</div>
</div>
`;
chatHistory.appendChild(messageDiv);
}
scrollToBottom();
}
// ===== 解析SVG响应 =====
function parseSVGResponse(response) {
const svgRegex = /```svg\s*([\s\S]*?)```/i;
const match = response.match(svgRegex);
if (match) {
const svgContent = match[1].trim();
const beforeText = response.substring(0, match.index).trim();
const afterText = response.substring(match.index + match[0].length).trim();
return {
svgContent,
beforeText,
afterText
};
}
return {
svgContent: null,
beforeText: response,
afterText: ''
};
}
// ===== 显示SVG =====
function viewSVG(svgId) {
if (!svgStorage[svgId]) {
console.error('SVG not found:', svgId);
return;
}
currentSvgId = svgId;
const svgContent = svgStorage[svgId].content;
svgViewer.innerHTML = svgContent;
}
// ===== 气泡操作功能 =====
function rollbackToMessage(messageId) {
console.log('退回到消息:', messageId);
// TODO: 实现退回逻辑
}
function regenerateMessage(messageId) {
console.log('重新生成消息:', messageId);
// TODO: 实现重新生成逻辑
}
// ===== 底部操作按钮 =====
document.getElementById('download-svg-btn').addEventListener('click', () => {
if (!currentSvgId) {
alert('请先生成SVG图表');
return;
}
const svgContent = svgStorage[currentSvgId].content;
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${currentMode}-${Date.now()}.svg`;
a.click();
URL.revokeObjectURL(url);
});
document.getElementById('export-image-btn').addEventListener('click', () => {
if (!currentSvgId) {
alert('请先生成SVG图表');
return;
}
console.log('导出为图片:', currentSvgId);
// TODO: 实现SVG转PNG功能
});
document.getElementById('view-code-btn').addEventListener('click', () => {
if (!currentSvgId) {
alert('请先生成SVG图表');
return;
}
const svgContent = svgStorage[currentSvgId].content;
alert('SVG代码\n\n' + svgContent);
// TODO: 使用更好的代码展示弹窗
});
// ===== 工具函数 =====
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function scrollToBottom() {
chatHistory.scrollTop = chatHistory.scrollHeight;
}
// ===== 模拟API响应测试用 =====
function simulateAPIResponse(userMessage) {
const mockResponses = [
`好的!我为您生成了一个${currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}
\`\`\`svg
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text x="300" y="200" text-anchor="middle" font-size="24" fill="#333">
这是${currentMode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG
</text>
<circle cx="300" cy="250" r="50" fill="#667eea" opacity="0.5"/>
</svg>
\`\`\`
包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
`已经为您调整完成!
\`\`\`svg
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#grad1)"/>
<text x="300" y="200" text-anchor="middle" font-size="28" fill="white" font-weight="bold">
${currentMode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'}
</text>
</svg>
\`\`\`
采用了更加鲜明的色彩组合,希望您满意!`
];
const response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
addAIMessage(response);
}
// ===== 页面加载时初始化 =====
loadConfig();
</script>
</body>
</html>
```