first commit
This commit is contained in:
182
css/style.css
Normal file
182
css/style.css
Normal 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
192
index.html
Normal 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
299
js/apiclient.js
Normal 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
767
js/app.js
Normal 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
289
js/utils.js
Normal 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
194
prompts/canvas-prompt.txt
Normal 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
144
prompts/swot-prompt.txt
Normal 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
99
功能概述.md
Normal 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
806
设计/原型.html
Normal 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>
|
||||
```
|
||||
Reference in New Issue
Block a user