6. 增加一个svg放大缩小功能;
7. 目前的svg下载,下载图片,显示代码实现有问题,好像没找到svg;再增加一个复制图片到剪切板功能; 8. 点重新生成按钮,应该再添加一个气泡啊,而且流式响应,现在点击重新生成,就时等待,没有实时显示; 9. 点击查看画布,没有区分现在显示的是哪个气泡或占位符,需要标记区分下;
This commit is contained in:
@@ -223,6 +223,16 @@ body {
|
|||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svg-placeholder-active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-content-wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
transform-origin: center top;
|
||||||
|
}
|
||||||
|
|
||||||
/* 小手摇摆动画 */
|
/* 小手摇摆动画 */
|
||||||
@keyframes wave {
|
@keyframes wave {
|
||||||
0%, 100% {transform: translateX(0px) rotate(90deg);}
|
0%, 100% {transform: translateX(0px) rotate(90deg);}
|
||||||
@@ -344,4 +354,8 @@ body {
|
|||||||
|
|
||||||
.clear-history-btn:hover iconify-icon {
|
.clear-history-btn:hover iconify-icon {
|
||||||
animation: shake 0.5s ease-in-out;
|
animation: shake 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#send-button.terminate-mode {
|
||||||
|
border-color: #dc2626;
|
||||||
|
}
|
||||||
|
|||||||
410
index.html
410
index.html
@@ -1,199 +1,211 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>产品画布 / SWOT分析</title>
|
<title>产品画布 / SWOT分析</title>
|
||||||
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
|
<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>
|
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 h-screen flex flex-col">
|
<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">
|
<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">
|
<div class="flex items-center space-x-2">
|
||||||
<iconify-icon icon="ph:lightning-fill" class="text-3xl text-white"></iconify-icon>
|
<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>
|
<h1 id="page-title" class="text-2xl font-black text-white tracking-tight">产品画布</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧按钮组 -->
|
<!-- 右侧按钮组 -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- API配置按钮 -->
|
<!-- 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配置">
|
<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>
|
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="text-white font-bold text-sm">点击切换模式</span>
|
<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>
|
<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">
|
<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>
|
<iconify-icon icon="ph:pen-nib-duotone" class="align-middle mr-1"></iconify-icon>
|
||||||
产品画布
|
产品画布
|
||||||
</button>
|
</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">
|
<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>
|
<iconify-icon icon="ph:chart-bar-duotone" class="align-middle mr-1"></iconify-icon>
|
||||||
SWOT分析
|
SWOT分析
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<main class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 overflow-hidden">
|
<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 class="md:col-span-1 bg-white wild-border border-cyan-500 flex flex-col">
|
||||||
<!-- 对话历史顶部栏 -->
|
<!-- 对话历史顶部栏 -->
|
||||||
<div class="p-3 border-b-3 border-gray-300 bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-between">
|
<div class="p-3 border-b-3 border-gray-300 bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<iconify-icon icon="ph:chats-circle-fill" class="text-2xl text-white"></iconify-icon>
|
<iconify-icon icon="ph:chats-circle-fill" class="text-2xl text-white"></iconify-icon>
|
||||||
<span class="font-black text-white">对话历史</span>
|
<span class="font-black text-white">对话历史</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="clear-history-btn" class="clear-history-btn bg-red-500 text-white px-3 py-1 border-2 border-black hover:bg-red-600 transition-all flex items-center gap-1 font-bold" title="清空对话历史">
|
<button id="clear-history-btn" class="clear-history-btn bg-red-500 text-white px-3 py-1 border-2 border-black hover:bg-red-600 transition-all flex items-center gap-1 font-bold" title="清空对话历史">
|
||||||
<iconify-icon icon="ph:trash-bold" class="text-lg"></iconify-icon>
|
<iconify-icon icon="ph:trash-bold" class="text-lg"></iconify-icon>
|
||||||
<span class="text-sm">清空</span>
|
<span class="text-sm">清空</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- 对话历史区 -->
|
<!-- 对话历史区 -->
|
||||||
<div id="chat-history" class="flex-1 p-4 overflow-y-auto space-y-3">
|
<div id="chat-history" class="flex-1 p-4 overflow-y-auto space-y-3">
|
||||||
<!-- 欢迎消息 -->
|
<!-- 欢迎消息 -->
|
||||||
<div class="flex justify-start">
|
<div class="flex justify-start">
|
||||||
<div class="chat-bubble-ai">
|
<div class="chat-bubble-ai">
|
||||||
👋 欢迎使用产品画布/SWOT分析工具!请输入您的需求,我将为您生成专业的分析图表。
|
👋 欢迎使用产品画布/SWOT分析工具!请输入您的需求,我将为您生成专业的分析图表。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入区 -->
|
<!-- 输入区 -->
|
||||||
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
|
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
|
||||||
<div class="relative flex items-center gap-2">
|
<div class="relative flex items-center gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
id="chat-input"
|
id="chat-input"
|
||||||
placeholder="输入您的想法,按Enter发送,Shift+Enter换行..."
|
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"
|
class="flex-1 auto-resize-input border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
|
||||||
rows="1"
|
rows="1"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button id="send-button" class="text-cyan-600 hover:text-cyan-700 transition-colors p-2 hover:scale-110 transform duration-200">
|
<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>
|
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧显示面板 -->
|
<!-- 右侧显示面板 -->
|
||||||
<div class="md:col-span-2 bg-white wild-border border-purple-600 flex flex-col">
|
<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-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">
|
<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>
|
<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>
|
<p class="mt-2 font-bold" id="placeholder-text">生成的产品画布将在此处显示</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
<!-- 底部操作栏 -->
|
||||||
<div class="p-3 border-t-3 border-gray-300 flex justify-end items-center gap-2 bg-gray-800">
|
<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">
|
<button id="zoom-out-btn" class="p-2 bg-white text-gray-700 border-2 border-black hover:bg-gray-200 transition-all" title="缩小">
|
||||||
<iconify-icon icon="mdi:download-outline" class="text-xl"></iconify-icon>
|
<iconify-icon icon="ph:magnifying-glass-minus-bold" class="text-xl"></iconify-icon>
|
||||||
</button>
|
</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="导出为图片">
|
<button id="zoom-in-btn" class="p-2 bg-white text-gray-700 border-2 border-black hover:bg-gray-200 transition-all" title="放大">
|
||||||
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
|
<iconify-icon icon="ph:magnifying-glass-plus-bold" class="text-xl"></iconify-icon>
|
||||||
</button>
|
</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="查看代码">
|
<button id="zoom-reset-btn" class="p-2 bg-white text-gray-700 border-2 border-black hover:bg-gray-200 transition-all" title="重置缩放">
|
||||||
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
|
<iconify-icon icon="ph:arrow-counter-clockwise-bold" class="text-xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<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">
|
||||||
</div>
|
<iconify-icon icon="mdi:download-outline" class="text-xl"></iconify-icon>
|
||||||
|
</button>
|
||||||
</main>
|
<button id="copy-image-btn" class="p-2 bg-yellow-500 text-white border-2 border-black hover:bg-yellow-600 transition-all" title="复制图片到剪贴板">
|
||||||
|
<iconify-icon icon="ph:clipboard-bold" class="text-xl"></iconify-icon>
|
||||||
<!-- API配置模态窗 -->
|
</button>
|
||||||
<div id="config-modal" class="modal-overlay">
|
<button id="export-image-btn" class="p-2 bg-green-500 text-white border-2 border-black hover:bg-green-600 transition-all" title="导出为图片">
|
||||||
<div class="modal-content">
|
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
|
||||||
<!-- 模态窗头部 -->
|
</button>
|
||||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-4 border-b-4 border-black flex items-center justify-between">
|
<button id="view-code-btn" class="p-2 bg-blue-500 text-white border-2 border-black hover:bg-blue-600 transition-all" title="查看代码">
|
||||||
<div class="flex items-center gap-2">
|
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
|
||||||
<iconify-icon icon="ph:plugs-connected-fill" class="text-3xl text-white"></iconify-icon>
|
</button>
|
||||||
<h2 class="text-xl font-black text-white">API 配置</h2>
|
</div>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
</button>
|
|
||||||
</div>
|
<!-- API配置模态窗 -->
|
||||||
|
<div id="config-modal" class="modal-overlay">
|
||||||
<!-- 模态窗内容 -->
|
<div class="modal-content">
|
||||||
<div class="p-6 space-y-4">
|
<!-- 模态窗头部 -->
|
||||||
<!-- API URL -->
|
<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>
|
<div class="flex items-center gap-2">
|
||||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<iconify-icon icon="ph:plugs-connected-fill" class="text-3xl text-white"></iconify-icon>
|
||||||
<iconify-icon icon="ph:link-bold" class="text-lg text-blue-600"></iconify-icon>
|
<h2 class="text-xl font-black text-white">API 配置</h2>
|
||||||
API URL
|
</div>
|
||||||
</label>
|
<button id="close-modal-btn" class="text-white hover:bg-white/20 p-2 transition-all">
|
||||||
<input
|
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
|
||||||
id="api-url"
|
</button>
|
||||||
type="text"
|
</div>
|
||||||
placeholder="https://api.example.com/v1/chat"
|
|
||||||
class="config-input"
|
<!-- 模态窗内容 -->
|
||||||
value=""
|
<div class="p-6 space-y-4">
|
||||||
/>
|
<!-- API URL -->
|
||||||
</div>
|
<div>
|
||||||
|
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||||
<!-- API Key -->
|
<iconify-icon icon="ph:link-bold" class="text-lg text-blue-600"></iconify-icon>
|
||||||
<div>
|
API URL
|
||||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
</label>
|
||||||
<iconify-icon icon="ph:key-bold" class="text-lg text-green-600"></iconify-icon>
|
<input
|
||||||
API Key
|
id="api-url"
|
||||||
</label>
|
type="text"
|
||||||
<input
|
placeholder="https://api.example.com/v1/chat"
|
||||||
id="api-key"
|
class="config-input"
|
||||||
type="password"
|
value=""
|
||||||
placeholder="sk-xxxxxxxxxxxxxxxx"
|
/>
|
||||||
class="config-input"
|
</div>
|
||||||
value=""
|
|
||||||
/>
|
<!-- API Key -->
|
||||||
</div>
|
<div>
|
||||||
|
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||||
<!-- Model -->
|
<iconify-icon icon="ph:key-bold" class="text-lg text-green-600"></iconify-icon>
|
||||||
<div>
|
API Key
|
||||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
</label>
|
||||||
<iconify-icon icon="ph:robot-bold" class="text-lg text-purple-600"></iconify-icon>
|
<input
|
||||||
模型 (Model)
|
id="api-key"
|
||||||
</label>
|
type="password"
|
||||||
<input
|
placeholder="sk-xxxxxxxxxxxxxxxx"
|
||||||
id="api-model"
|
class="config-input"
|
||||||
type="text"
|
value=""
|
||||||
placeholder="gpt-4-turbo"
|
/>
|
||||||
class="config-input"
|
</div>
|
||||||
value=""
|
|
||||||
/>
|
<!-- Model -->
|
||||||
</div>
|
<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>
|
||||||
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
|
模型 (Model)
|
||||||
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
</label>
|
||||||
<span id="status-text">等待操作...</span>
|
<input
|
||||||
</div>
|
id="api-model"
|
||||||
</div>
|
type="text"
|
||||||
|
placeholder="gpt-4-turbo"
|
||||||
<!-- 模态窗底部按钮 -->
|
class="config-input"
|
||||||
<div class="p-4 border-t-3 border-gray-300 bg-gray-100 flex gap-3 justify-end">
|
value=""
|
||||||
<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>
|
</div>
|
||||||
测试连接
|
|
||||||
</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">
|
<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:floppy-disk-bold"></iconify-icon>
|
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||||
保存配置
|
<span id="status-text">等待操作...</span>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<!-- 模态窗底部按钮 -->
|
||||||
|
<div class="p-4 border-t-3 border-gray-300 bg-gray-100 flex gap-3 justify-end">
|
||||||
<!-- 引入JavaScript文件 -->
|
<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">
|
||||||
<script src="js/utils.js"></script>
|
<iconify-icon icon="ph:flask-bold"></iconify-icon>
|
||||||
<script src="js/apiclient.js"></script>
|
测试连接
|
||||||
<script src="js/app.js"></script>
|
</button>
|
||||||
</body>
|
<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">
|
||||||
</html>
|
<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>
|
||||||
|
|||||||
437
js/app.js
437
js/app.js
@@ -21,6 +21,11 @@ class ProductCanvasApp {
|
|||||||
this.conversationHistory = {};
|
this.conversationHistory = {};
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
this.activeStreamHandle = null;
|
this.activeStreamHandle = null;
|
||||||
|
this.svgZoom = { canvas: 1, swot: 1 };
|
||||||
|
this.activeSvgPlaceholder = null;
|
||||||
|
this.pendingSvgId = null;
|
||||||
|
this.pendingCancel = false;
|
||||||
|
this.copyClipboardSupported = typeof ClipboardItem !== 'undefined' && !!navigator.clipboard;
|
||||||
|
|
||||||
this.initElements();
|
this.initElements();
|
||||||
this.initEventListeners();
|
this.initEventListeners();
|
||||||
@@ -51,7 +56,11 @@ class ProductCanvasApp {
|
|||||||
this.placeholderText = this.svgViewer.querySelector('#placeholder-text');
|
this.placeholderText = this.svgViewer.querySelector('#placeholder-text');
|
||||||
|
|
||||||
// 底部操作按钮
|
// 底部操作按钮
|
||||||
|
this.zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||||
|
this.zoomInBtn = document.getElementById('zoom-in-btn');
|
||||||
|
this.zoomResetBtn = document.getElementById('zoom-reset-btn');
|
||||||
this.downloadSvgBtn = document.getElementById('download-svg-btn');
|
this.downloadSvgBtn = document.getElementById('download-svg-btn');
|
||||||
|
this.copyImageBtn = document.getElementById('copy-image-btn');
|
||||||
this.exportImageBtn = document.getElementById('export-image-btn');
|
this.exportImageBtn = document.getElementById('export-image-btn');
|
||||||
this.viewCodeBtn = document.getElementById('view-code-btn');
|
this.viewCodeBtn = document.getElementById('view-code-btn');
|
||||||
|
|
||||||
@@ -100,7 +109,18 @@ class ProductCanvasApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 底部操作按钮
|
// 底部操作按钮
|
||||||
|
if (this.zoomOutBtn) this.zoomOutBtn.addEventListener('click', () => this.adjustSvgZoom(-0.25));
|
||||||
|
if (this.zoomInBtn) this.zoomInBtn.addEventListener('click', () => this.adjustSvgZoom(0.25));
|
||||||
|
if (this.zoomResetBtn) this.zoomResetBtn.addEventListener('click', () => this.resetSvgZoom());
|
||||||
this.downloadSvgBtn.addEventListener('click', () => this.downloadSVG());
|
this.downloadSvgBtn.addEventListener('click', () => this.downloadSVG());
|
||||||
|
if (this.copyImageBtn) {
|
||||||
|
if (this.copyClipboardSupported) {
|
||||||
|
this.copyImageBtn.addEventListener('click', () => this.copySvgToClipboard());
|
||||||
|
} else {
|
||||||
|
this.copyImageBtn.disabled = true;
|
||||||
|
this.copyImageBtn.title = '当前浏览器不支持复制图片到剪贴板';
|
||||||
|
}
|
||||||
|
}
|
||||||
this.exportImageBtn.addEventListener('click', () => this.exportAsImage());
|
this.exportImageBtn.addEventListener('click', () => this.exportAsImage());
|
||||||
this.viewCodeBtn.addEventListener('click', () => this.viewSVGCode());
|
this.viewCodeBtn.addEventListener('click', () => this.viewSVGCode());
|
||||||
|
|
||||||
@@ -141,6 +161,7 @@ class ProductCanvasApp {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.renderSvgViewerForMode();
|
this.renderSvgViewerForMode();
|
||||||
|
this.setSendButtonState('idle');
|
||||||
|
|
||||||
// 加载API配置
|
// 加载API配置
|
||||||
const apiConfig = window.apiClient.getConfig();
|
const apiConfig = window.apiClient.getConfig();
|
||||||
@@ -236,6 +257,8 @@ class ProductCanvasApp {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.placeholderText = this.svgViewer.querySelector('#placeholder-text');
|
this.placeholderText = this.svgViewer.querySelector('#placeholder-text');
|
||||||
|
this.setActivePlaceholder(null);
|
||||||
|
this.updateZoomButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSvgViewerForMode() {
|
renderSvgViewerForMode() {
|
||||||
@@ -257,52 +280,175 @@ class ProductCanvasApp {
|
|||||||
|
|
||||||
if (latestSvgId && svgStore[latestSvgId]) {
|
if (latestSvgId && svgStore[latestSvgId]) {
|
||||||
this.currentSvgId = latestSvgId;
|
this.currentSvgId = latestSvgId;
|
||||||
this.svgViewer.innerHTML = svgStore[latestSvgId].content;
|
this.mountSvgMarkup(svgStore[latestSvgId].content);
|
||||||
this.placeholderText = null;
|
this.setActivePlaceholder(latestSvgId);
|
||||||
} else {
|
} else {
|
||||||
|
this.currentSvgId = null;
|
||||||
this.showSvgPlaceholder();
|
this.showSvgPlaceholder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateZoomButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustSvgZoom(delta) {
|
||||||
|
const current = this.svgZoom[this.currentMode] || 1;
|
||||||
|
const next = Math.min(3, Math.max(0.25, parseFloat((current + delta).toFixed(2))));
|
||||||
|
this.svgZoom[this.currentMode] = next;
|
||||||
|
this.applySvgZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSvgZoom() {
|
||||||
|
this.svgZoom[this.currentMode] = 1;
|
||||||
|
this.applySvgZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
applySvgZoom() {
|
||||||
|
const zoom = this.svgZoom[this.currentMode] || 1;
|
||||||
|
const wrapper = this.svgViewer.querySelector('.svg-content-wrapper');
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.style.transform = `scale(${zoom})`;
|
||||||
|
wrapper.style.transformOrigin = 'center top';
|
||||||
|
}
|
||||||
|
this.updateZoomButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateZoomButtons() {
|
||||||
|
if (!this.zoomInBtn || !this.zoomOutBtn || !this.zoomResetBtn) return;
|
||||||
|
const hasActiveSvg = !!this.currentSvgId && !!(this.svgStorage[this.currentMode] || {})[this.currentSvgId];
|
||||||
|
const zoom = this.svgZoom[this.currentMode] || 1;
|
||||||
|
|
||||||
|
const disableControls = !hasActiveSvg;
|
||||||
|
this.zoomInBtn.disabled = disableControls || zoom >= 3;
|
||||||
|
this.zoomOutBtn.disabled = disableControls || zoom <= 0.25;
|
||||||
|
this.zoomResetBtn.disabled = disableControls || Math.abs(zoom - 1) < 0.01;
|
||||||
|
|
||||||
|
if (!hasActiveSvg) {
|
||||||
|
if (this.copyImageBtn) this.copyImageBtn.disabled = true;
|
||||||
|
this.downloadSvgBtn.disabled = true;
|
||||||
|
this.exportImageBtn.disabled = true;
|
||||||
|
this.viewCodeBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
if (this.copyImageBtn) this.copyImageBtn.disabled = !this.copyClipboardSupported;
|
||||||
|
this.downloadSvgBtn.disabled = false;
|
||||||
|
this.exportImageBtn.disabled = false;
|
||||||
|
this.viewCodeBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActivePlaceholder(svgId) {
|
||||||
|
const previousActive = this.chatHistory.querySelectorAll('.svg-placeholder-active');
|
||||||
|
previousActive.forEach(el => el.classList.remove('svg-placeholder-active'));
|
||||||
|
if (svgId) {
|
||||||
|
const next = this.chatHistory.querySelector(`.svg-placeholder-block[data-svg-id="${svgId}"], .svg-drawing-placeholder[data-svg-id="${svgId}"]`);
|
||||||
|
if (next) {
|
||||||
|
next.classList.add('svg-placeholder-active');
|
||||||
|
this.activeSvgPlaceholder = svgId;
|
||||||
|
} else {
|
||||||
|
this.activeSvgPlaceholder = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.activeSvgPlaceholder = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mountSvgMarkup(svgMarkup, temporary = false) {
|
||||||
|
this.svgViewer.innerHTML = '';
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'svg-content-wrapper';
|
||||||
|
wrapper.innerHTML = svgMarkup;
|
||||||
|
this.svgViewer.appendChild(wrapper);
|
||||||
|
this.placeholderText = null;
|
||||||
|
if (!temporary) {
|
||||||
|
this.applySvgZoom();
|
||||||
|
} else {
|
||||||
|
const zoom = this.svgZoom[this.currentMode] || 1;
|
||||||
|
wrapper.style.transform = `scale(${zoom})`;
|
||||||
|
wrapper.style.transformOrigin = 'center top';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSvgContent(svgId) {
|
||||||
|
const store = this.svgStorage[this.currentMode] || {};
|
||||||
|
if (!svgId || !store[svgId]) {
|
||||||
|
this.showSvgPlaceholder();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentSvgId = svgId;
|
||||||
|
this.mountSvgMarkup(store[svgId].content);
|
||||||
|
this.setActivePlaceholder(svgId);
|
||||||
|
this.applySvgZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTemporarySvg(svgMarkup) {
|
||||||
|
this.mountSvgMarkup(svgMarkup, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildContextForUserMessage(userIndex) {
|
||||||
|
const history = this.conversationHistory[this.currentMode] || [];
|
||||||
|
if (userIndex < 0 || userIndex >= history.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = history[userIndex];
|
||||||
|
if (!target || target.type !== 'user') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.max(0, userIndex - 10);
|
||||||
|
const contextSlice = history.slice(start, userIndex);
|
||||||
|
const contextMessages = contextSlice
|
||||||
|
.filter(msg => msg.type === 'user' || msg.type === 'ai')
|
||||||
|
.map(msg => ({
|
||||||
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
|
content: msg.content
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
userMessage: target.content,
|
||||||
|
contextMessages
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
const message = this.chatInput.value.trim();
|
const message = this.chatInput.value.trim();
|
||||||
if (!message || this.isProcessing) return;
|
if (!message || this.isProcessing) return;
|
||||||
|
|
||||||
// 检查API配置
|
|
||||||
if (!window.apiClient.isConfigValid()) {
|
if (!window.apiClient.isConfigValid()) {
|
||||||
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
|
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
|
||||||
this.openConfigModal();
|
this.openConfigModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pendingCancel = false;
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
this.setSendButtonState('busy');
|
|
||||||
this.sendButton.disabled = true;
|
|
||||||
|
|
||||||
// 添加用户消息
|
|
||||||
this.addUserMessage(message);
|
this.addUserMessage(message);
|
||||||
this.chatInput.value = '';
|
this.chatInput.value = '';
|
||||||
Utils.autoResizeTextarea(this.chatInput);
|
Utils.autoResizeTextarea(this.chatInput);
|
||||||
|
|
||||||
|
const history = this.conversationHistory[this.currentMode] || [];
|
||||||
|
const targetIndex = history.length - 1;
|
||||||
|
const payload = this.buildContextForUserMessage(targetIndex);
|
||||||
|
if (!payload) {
|
||||||
|
console.warn('无法构建上下文,终止发送');
|
||||||
|
this.isProcessing = false;
|
||||||
|
this.setSendButtonState('idle');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setSendButtonState('streaming');
|
||||||
|
this.sendButton.disabled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取对话上下文
|
await this.startStreamingMessage(payload.userMessage, payload.contextMessages);
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('发送消息失败:', error);
|
console.error('发送消息失败:', error);
|
||||||
this.addErrorMessage(error.message);
|
this.addErrorMessage(error.message);
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
this.setSendButtonState('idle');
|
this.setSendButtonState('idle');
|
||||||
this.activeStreamHandle = null;
|
this.activeStreamHandle = null;
|
||||||
|
this.sendButton.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +485,9 @@ class ProductCanvasApp {
|
|||||||
|
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
this.setSendButtonState('idle');
|
this.setSendButtonState('idle');
|
||||||
|
this.sendButton.disabled = false;
|
||||||
this.activeStreamHandle = null;
|
this.activeStreamHandle = null;
|
||||||
|
this.pendingCancel = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChunk = (chunk) => {
|
const onChunk = (chunk) => {
|
||||||
@@ -363,11 +511,13 @@ class ProductCanvasApp {
|
|||||||
if (svgStartMatch) {
|
if (svgStartMatch) {
|
||||||
svgStarted = true;
|
svgStarted = true;
|
||||||
svgId = svgId || Utils.generateId('svg');
|
svgId = svgId || Utils.generateId('svg');
|
||||||
|
this.pendingSvgId = svgId;
|
||||||
|
|
||||||
const svgStartIndex = svgStartMatch.index;
|
const svgStartIndex = svgStartMatch.index;
|
||||||
beforeText = fullContent.substring(0, svgStartIndex);
|
beforeText = fullContent.substring(0, svgStartIndex);
|
||||||
|
|
||||||
this.updateStreamingMessageWithPlaceholder(messageContainer, beforeText, svgId);
|
this.updateStreamingMessageWithPlaceholder(messageContainer, beforeText, svgId);
|
||||||
|
this.setActivePlaceholder(svgId);
|
||||||
|
|
||||||
this.svgViewer.innerHTML = `
|
this.svgViewer.innerHTML = `
|
||||||
<div class="flex items-center justify-center h-full">
|
<div class="flex items-center justify-center h-full">
|
||||||
@@ -377,6 +527,7 @@ class ProductCanvasApp {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
this.updateZoomButtons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,8 +544,6 @@ class ProductCanvasApp {
|
|||||||
svgContent += '</svg>';
|
svgContent += '</svg>';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.svgViewer.innerHTML = svgContent;
|
|
||||||
|
|
||||||
this.svgStorage[this.currentMode][svgId] = {
|
this.svgStorage[this.currentMode][svgId] = {
|
||||||
content: svgContent,
|
content: svgContent,
|
||||||
messageId,
|
messageId,
|
||||||
@@ -402,6 +551,8 @@ class ProductCanvasApp {
|
|||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.pendingSvgId = null;
|
||||||
|
this.renderSvgContent(svgId);
|
||||||
this.updatePlaceholderToClickable(messageContainer, svgId);
|
this.updatePlaceholderToClickable(messageContainer, svgId);
|
||||||
|
|
||||||
svgStarted = false;
|
svgStarted = false;
|
||||||
@@ -420,7 +571,7 @@ class ProductCanvasApp {
|
|||||||
tempSvgContent += '</svg>';
|
tempSvgContent += '</svg>';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.svgViewer.innerHTML = tempSvgContent;
|
this.renderTemporarySvg(tempSvgContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -441,6 +592,12 @@ class ProductCanvasApp {
|
|||||||
|
|
||||||
this.activeStreamHandle = streamHandle;
|
this.activeStreamHandle = streamHandle;
|
||||||
|
|
||||||
|
if (this.pendingCancel) {
|
||||||
|
const shouldCancel = this.pendingCancel;
|
||||||
|
this.pendingCancel = false;
|
||||||
|
this.cancelActiveStream();
|
||||||
|
}
|
||||||
|
|
||||||
await streamHandle.finished;
|
await streamHandle.finished;
|
||||||
|
|
||||||
if (!streamClosed) {
|
if (!streamClosed) {
|
||||||
@@ -451,6 +608,8 @@ class ProductCanvasApp {
|
|||||||
this.activeStreamHandle = null;
|
this.activeStreamHandle = null;
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
this.setSendButtonState('idle');
|
this.setSendButtonState('idle');
|
||||||
|
this.sendButton.disabled = false;
|
||||||
|
this.pendingCancel = false;
|
||||||
|
|
||||||
const bubble = this.chatHistory.querySelector(`[data-message-id="${messageId}"]`);
|
const bubble = this.chatHistory.querySelector(`[data-message-id="${messageId}"]`);
|
||||||
if (bubble) {
|
if (bubble) {
|
||||||
@@ -469,9 +628,12 @@ class ProductCanvasApp {
|
|||||||
|
|
||||||
cancelActiveStream() {
|
cancelActiveStream() {
|
||||||
if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') {
|
if (!this.activeStreamHandle || typeof this.activeStreamHandle.cancel !== 'function') {
|
||||||
|
this.pendingCancel = true;
|
||||||
|
this.setSendButtonState('terminating');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pendingCancel = false;
|
||||||
this.setSendButtonState('terminating');
|
this.setSendButtonState('terminating');
|
||||||
try {
|
try {
|
||||||
this.activeStreamHandle.cancel();
|
this.activeStreamHandle.cancel();
|
||||||
@@ -532,22 +694,26 @@ class ProductCanvasApp {
|
|||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
placeholder.classList.remove('svg-drawing-placeholder');
|
placeholder.classList.remove('svg-drawing-placeholder');
|
||||||
placeholder.classList.add('svg-placeholder-block');
|
placeholder.classList.add('svg-placeholder-block');
|
||||||
|
placeholder.setAttribute('data-svg-id', svgId);
|
||||||
placeholder.innerHTML = `📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG`;
|
placeholder.innerHTML = `📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG`;
|
||||||
placeholder.setAttribute('onclick', `app.viewSVG('${svgId}')`);
|
placeholder.setAttribute('onclick', `app.viewSVG('${svgId}')`);
|
||||||
|
if (this.currentSvgId === svgId) {
|
||||||
|
placeholder.classList.add('svg-placeholder-active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新SVG后的消息内容
|
// 更新SVG后的消息内容
|
||||||
updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) {
|
updateStreamingMessageAfterSVG(container, beforeText, svgId, afterText) {
|
||||||
// 使用Markdown解析文本
|
|
||||||
const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText);
|
const parsedBeforeText = typeof marked !== 'undefined' ? marked.parse(beforeText) : Utils.escapeHtml(beforeText);
|
||||||
const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText);
|
const parsedAfterText = typeof marked !== 'undefined' ? marked.parse(afterText) : Utils.escapeHtml(afterText);
|
||||||
|
const placeholderClass = this.currentSvgId === svgId ? 'svg-placeholder-block svg-placeholder-active' : 'svg-placeholder-block';
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="chat-bubble-ai relative streaming-text" data-message-id="${container.dataset.messageId}">
|
<div class="chat-bubble-ai relative streaming-text" data-message-id="${container.dataset.messageId}">
|
||||||
<div>
|
<div>
|
||||||
${parsedBeforeText}
|
${parsedBeforeText}
|
||||||
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
|
<div class="${placeholderClass}" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
|
||||||
📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
|
📊 点击查看${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
|
||||||
</div>
|
</div>
|
||||||
<div class="typing-cursor">${parsedAfterText}</div>
|
<div class="typing-cursor">${parsedAfterText}</div>
|
||||||
@@ -615,6 +781,7 @@ class ProductCanvasApp {
|
|||||||
|
|
||||||
// 完成流式消息
|
// 完成流式消息
|
||||||
finalizeStreamingMessage(messageId, fullContent, svgId = null) {
|
finalizeStreamingMessage(messageId, fullContent, svgId = null) {
|
||||||
|
this.pendingSvgId = null;
|
||||||
const parsed = Utils.parseSVGResponse(fullContent);
|
const parsed = Utils.parseSVGResponse(fullContent);
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
@@ -676,6 +843,7 @@ class ProductCanvasApp {
|
|||||||
|
|
||||||
// 清空当前模式的SVG存储
|
// 清空当前模式的SVG存储
|
||||||
this.svgStorage[this.currentMode] = {};
|
this.svgStorage[this.currentMode] = {};
|
||||||
|
this.svgZoom[this.currentMode] = 1;
|
||||||
this.showSvgPlaceholder();
|
this.showSvgPlaceholder();
|
||||||
|
|
||||||
// 保存数据
|
// 保存数据
|
||||||
@@ -804,11 +972,12 @@ class ProductCanvasApp {
|
|||||||
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
|
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
|
||||||
|
|
||||||
messageDiv.className = 'flex justify-start';
|
messageDiv.className = 'flex justify-start';
|
||||||
|
const placeholderClass = this.currentSvgId === svgId ? 'svg-placeholder-block svg-placeholder-active' : 'svg-placeholder-block';
|
||||||
messageDiv.innerHTML = `
|
messageDiv.innerHTML = `
|
||||||
<div class="chat-bubble-ai relative" data-message-id="${message.id}">
|
<div class="chat-bubble-ai relative" data-message-id="${message.id}">
|
||||||
<div>
|
<div>
|
||||||
${beforeHtml}
|
${beforeHtml}
|
||||||
<div class="svg-placeholder-block" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
|
<div class="${placeholderClass}" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
|
||||||
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
|
📊 点击查看 ${this.currentMode === 'canvas' ? '产品画布' : 'SWOT分析'} SVG
|
||||||
</div>
|
</div>
|
||||||
${afterHtml}
|
${afterHtml}
|
||||||
@@ -889,19 +1058,19 @@ class ProductCanvasApp {
|
|||||||
Utils.storage.set('swotHistory', this.conversationHistory.swot || []);
|
Utils.storage.set('swotHistory', this.conversationHistory.swot || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setActivePlaceholder(this.currentSvgId);
|
||||||
Utils.scrollToBottom(this.chatHistory);
|
Utils.scrollToBottom(this.chatHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示SVG
|
// 显示SVG
|
||||||
viewSVG(svgId) {
|
viewSVG(svgId) {
|
||||||
if (!this.svgStorage[this.currentMode][svgId]) {
|
const store = this.svgStorage[this.currentMode] || {};
|
||||||
|
if (!store[svgId]) {
|
||||||
console.error('SVG not found:', svgId);
|
console.error('SVG not found:', svgId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentSvgId = svgId;
|
this.renderSvgContent(svgId);
|
||||||
const svgContent = this.svgStorage[this.currentMode][svgId].content;
|
|
||||||
this.svgViewer.innerHTML = svgContent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 退回到指定消息
|
// 退回到指定消息
|
||||||
@@ -943,64 +1112,194 @@ class ProductCanvasApp {
|
|||||||
// 重新生成消息
|
// 重新生成消息
|
||||||
async regenerateMessage(messageId) {
|
async regenerateMessage(messageId) {
|
||||||
if (this.isProcessing) return;
|
if (this.isProcessing) return;
|
||||||
|
|
||||||
|
const history = this.conversationHistory[this.currentMode] || [];
|
||||||
|
const targetIndex = history.findIndex(msg => msg.id === messageId && msg.type === 'ai');
|
||||||
|
if (targetIndex === -1) {
|
||||||
|
console.warn('未找到可重新生成的消息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userIndex = -1;
|
||||||
|
for (let i = targetIndex - 1; i >= 0; i--) {
|
||||||
|
if (history[i].type === 'user') {
|
||||||
|
userIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIndex === -1) {
|
||||||
|
alert('未找到对应的用户消息,无法重新生成');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = this.buildContextForUserMessage(userIndex);
|
||||||
|
if (!payload) {
|
||||||
|
alert('无法构建对话上下文,请稍后重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingCancel = false;
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
this.setSendButtonState('busy');
|
this.setSendButtonState('streaming');
|
||||||
this.sendButton.disabled = true;
|
this.sendButton.disabled = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 重新生成响应
|
await this.startStreamingMessage(payload.userMessage, payload.contextMessages);
|
||||||
const response = await window.apiClient.regenerateResponse(messageId, this.conversationHistory[this.currentMode]);
|
|
||||||
|
|
||||||
// 退回到指定消息
|
|
||||||
this.rollbackToMessage(messageId);
|
|
||||||
|
|
||||||
// 添加新的AI回复
|
|
||||||
this.addAIMessage(response);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重新生成失败:', error);
|
console.error('重新生成失败:', error);
|
||||||
this.addErrorMessage(error.message);
|
this.addErrorMessage(error.message);
|
||||||
} finally {
|
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
this.setSendButtonState('idle');
|
this.setSendButtonState('idle');
|
||||||
this.activeStreamHandle = null;
|
this.activeStreamHandle = null;
|
||||||
|
this.sendButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveSvgRecord(showWarning = true) {
|
||||||
|
const store = this.svgStorage[this.currentMode] || {};
|
||||||
|
if (this.currentSvgId && store[this.currentSvgId]) {
|
||||||
|
return { id: this.currentSvgId, ...store[this.currentSvgId] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showWarning) {
|
||||||
|
alert('请先生成并选择一个图表');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSvgDimensions(svgContent) {
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(svgContent, 'image/svg+xml');
|
||||||
|
const svgEl = doc.querySelector('svg');
|
||||||
|
if (!svgEl) {
|
||||||
|
return { width: 1024, height: 768 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseLength = (value) => {
|
||||||
|
if (!value) return null;
|
||||||
|
const match = /([0-9.]+)/.exec(value);
|
||||||
|
return match ? parseFloat(match[1]) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let width = parseLength(svgEl.getAttribute('width'));
|
||||||
|
let height = parseLength(svgEl.getAttribute('height'));
|
||||||
|
const viewBox = svgEl.getAttribute('viewBox');
|
||||||
|
|
||||||
|
if ((!width || !height) && viewBox) {
|
||||||
|
const parts = viewBox.split(/\s+/).map(Number).filter(n => !Number.isNaN(n));
|
||||||
|
if (parts.length === 4) {
|
||||||
|
width = width || parts[2];
|
||||||
|
height = height || parts[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: width || 1024,
|
||||||
|
height: height || 768
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('解析SVG尺寸失败:', error);
|
||||||
|
return { width: 1024, height: 768 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadImageFromUrl(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => resolve(img);
|
||||||
|
img.onerror = (err) => reject(err);
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async convertSvgToPngBlob(svgContent) {
|
||||||
|
const { width, height } = this.parseSvgDimensions(svgContent);
|
||||||
|
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||||
|
const url = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const img = await this.loadImageFromUrl(url);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const canvasWidth = Math.max(1, img.naturalWidth || width || 1024);
|
||||||
|
const canvasHeight = Math.max(1, img.naturalHeight || height || 768);
|
||||||
|
canvas.width = canvasWidth;
|
||||||
|
canvas.height = canvasHeight;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
} else {
|
||||||
|
reject(new Error('无法生成PNG图像'));
|
||||||
|
}
|
||||||
|
}, 'image/png');
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async copySvgToClipboard() {
|
||||||
|
const record = this.getActiveSvgRecord();
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
|
if (!navigator.clipboard || typeof ClipboardItem === 'undefined') {
|
||||||
|
alert('当前浏览器不支持复制图片到剪贴板,请使用下载功能。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await this.convertSvgToPngBlob(record.content);
|
||||||
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
||||||
|
alert('图像已复制到剪贴板');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制图片失败:', error);
|
||||||
|
alert('复制失败,请稍后重试。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载SVG
|
// 下载SVG
|
||||||
downloadSVG() {
|
downloadSVG() {
|
||||||
if (!this.currentSvgId) {
|
const record = this.getActiveSvgRecord();
|
||||||
alert('请先生成SVG图表');
|
if (!record) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgContent = this.svgStorage[this.currentMode][this.currentSvgId].content;
|
|
||||||
const filename = `${this.currentMode}-${Utils.formatDateTime().replace(/[/:]/g, '-')}.svg`;
|
const filename = `${this.currentMode}-${Utils.formatDateTime().replace(/[/:]/g, '-')}.svg`;
|
||||||
Utils.downloadFile(svgContent, filename, 'image/svg+xml');
|
Utils.downloadFile(record.content, filename, 'image/svg+xml');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出为图片
|
// 导出为图片
|
||||||
exportAsImage() {
|
async exportAsImage() {
|
||||||
if (!this.currentSvgId) {
|
const record = this.getActiveSvgRecord();
|
||||||
alert('请先生成SVG图表');
|
if (!record) return;
|
||||||
return;
|
|
||||||
|
try {
|
||||||
|
const blob = await this.convertSvgToPngBlob(record.content);
|
||||||
|
const filename = `${this.currentMode}-${Utils.formatDateTime().replace(/[/:]/g, '-')}.png`;
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出图片失败:', error);
|
||||||
|
alert('导出图片失败,请稍后重试。');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这里可以实现SVG转PNG的功能
|
|
||||||
// 由于需要额外的库,这里先提示用户
|
|
||||||
alert('SVG转PNG功能需要额外的库支持,您可以使用下载SVG功能,然后使用在线工具转换。');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看SVG代码
|
// 查看SVG代码
|
||||||
viewSVGCode() {
|
viewSVGCode() {
|
||||||
if (!this.currentSvgId) {
|
const record = this.getActiveSvgRecord();
|
||||||
alert('请先生成SVG图表');
|
if (!record) return;
|
||||||
return;
|
|
||||||
}
|
const svgContent = record.content;
|
||||||
|
|
||||||
const svgContent = this.svgStorage[this.currentMode][this.currentSvgId].content;
|
|
||||||
|
|
||||||
// 创建代码查看模态窗
|
// 创建代码查看模态窗
|
||||||
const modal = document.createElement('div');
|
const modal = document.createElement('div');
|
||||||
modal.className = 'modal-overlay active';
|
modal.className = 'modal-overlay active';
|
||||||
|
|||||||
Reference in New Issue
Block a user