Compare commits
8 Commits
2a3d16fca2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d74fbce137 | ||
|
|
0f68b6d7a2 | ||
|
|
b82297efc3 | ||
|
|
69a177b85a | ||
|
|
19afa19462 | ||
|
|
24adda3f14 | ||
|
|
c1802352f5 | ||
|
|
71cce80e2d |
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"liveServer.settings.port": 5501
|
||||||
|
}
|
||||||
17
README.md
17
README.md
@@ -12,12 +12,14 @@
|
|||||||
- **SWOT分析模式**:生成和展示SWOT分析图表
|
- **SWOT分析模式**:生成和展示SWOT分析图表
|
||||||
- **ECharts图表模式**:生成数据可视化图表
|
- **ECharts图表模式**:生成数据可视化图表
|
||||||
- **Mermaid图表模式**:生成流程图和关系图
|
- **Mermaid图表模式**:生成流程图和关系图
|
||||||
|
- **落地页生成模式**:基于提示词流式生成完整 HTML 落地页并实时预览
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
### 💬 AI对话交互系统
|
### 💬 AI对话交互系统
|
||||||
- **智能对话**:基于自然语言的产品分析请求
|
- **智能对话**:基于自然语言的产品分析请求
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
- **图片导出**:将当前图表导出为PNG图片格式
|
- **图片导出**:将当前图表导出为PNG图片格式
|
||||||
- **剪贴板复制**:复制图表图片到剪贴板
|
- **剪贴板复制**:复制图表图片到剪贴板
|
||||||
- **代码查看**:查看当前图表的SVG源代码
|
- **代码查看**:查看当前图表的SVG源代码
|
||||||
|
- **页面预览**:对落地页生成结果进行 98% 视窗尺寸的模态预览或一键新窗口打开
|
||||||
|
|
||||||
## **所有数据均由浏览器本地缓存,清空后就清空了💨**
|
## **所有数据均由浏览器本地缓存,清空后就清空了💨**
|
||||||
|
|
||||||
@@ -102,7 +105,8 @@ services:
|
|||||||
│ │ ├── product-canvas.js # 产品画布模块
|
│ │ ├── product-canvas.js # 产品画布模块
|
||||||
│ │ ├── swot.js # SWOT分析模块
|
│ │ ├── swot.js # SWOT分析模块
|
||||||
│ │ ├── echarts.js # ECharts图表模块
|
│ │ ├── echarts.js # ECharts图表模块
|
||||||
│ │ └── mermaid.js # Mermaid图表模块
|
│ │ ├── mermaid.js # Mermaid图表模块
|
||||||
|
│ │ └── onepage.js # 落地页生成模块
|
||||||
│ ├── services/ # 服务层
|
│ ├── services/ # 服务层
|
||||||
│ │ ├── conversation-service.js # 对话服务
|
│ │ ├── conversation-service.js # 对话服务
|
||||||
│ │ └── storage-service.js # 存储服务
|
│ │ └── storage-service.js # 存储服务
|
||||||
@@ -115,7 +119,8 @@ services:
|
|||||||
│ ├── canvas-prompt.txt # 产品画布提示词
|
│ ├── canvas-prompt.txt # 产品画布提示词
|
||||||
│ ├── swot-prompt.txt # SWOT分析提示词
|
│ ├── swot-prompt.txt # SWOT分析提示词
|
||||||
│ ├── echarts-prompt.txt # ECharts提示词
|
│ ├── echarts-prompt.txt # ECharts提示词
|
||||||
│ └── mermaid-prompt.txt # Mermaid提示词
|
│ ├── mermaid-prompt.txt # Mermaid提示词
|
||||||
|
│ └── onepage-prompt.txt # 落地页提示词
|
||||||
├── 设计/ # 设计文件
|
├── 设计/ # 设计文件
|
||||||
│ └── 原型.html # 设计原型
|
│ └── 原型.html # 设计原型
|
||||||
├── 功能概述.md # 功能说明文档
|
├── 功能概述.md # 功能说明文档
|
||||||
@@ -131,8 +136,8 @@ services:
|
|||||||
1. **选择模式**:点击顶部的模式按钮选择所需的分析类型
|
1. **选择模式**:点击顶部的模式按钮选择所需的分析类型
|
||||||
2. **输入需求**:在左侧对话框中描述您的产品或分析需求
|
2. **输入需求**:在左侧对话框中描述您的产品或分析需求
|
||||||
3. **生成图表**:AI将根据您的需求生成相应的分析图表
|
3. **生成图表**:AI将根据您的需求生成相应的分析图表
|
||||||
4. **查看图表**:点击对话中的图表占位符在右侧查看完整图表
|
4. **查看图表 / 页面**:点击对话中的占位符在右侧查看完整内容
|
||||||
5. **导出结果**:使用底部工具栏导出或复制图表
|
5. **导出结果**:使用底部工具栏导出、复制或全屏预览
|
||||||
|
|
||||||
### 产品画布模式
|
### 产品画布模式
|
||||||
|
|
||||||
@@ -180,6 +185,7 @@ SWOT分析模式帮助您评估项目的优势、劣势、机会和威胁:
|
|||||||
- **缩放控制**:使用工具栏按钮调整图表大小
|
- **缩放控制**:使用工具栏按钮调整图表大小
|
||||||
- **全屏查看**:在右侧面板获得更好的查看体验
|
- **全屏查看**:在右侧面板获得更好的查看体验
|
||||||
- **多格式导出**:支持SVG、PNG等多种导出格式
|
- **多格式导出**:支持SVG、PNG等多种导出格式
|
||||||
|
- **落地页预览**:使用“预览落地页”按钮打开 98% 视窗大小的模态窗口,可再一键在新标签页打开完整 HTML
|
||||||
|
|
||||||
## 🔧 开发指南
|
## 🔧 开发指南
|
||||||
|
|
||||||
@@ -194,6 +200,9 @@ SWOT分析模式帮助您评估项目的优势、劣势、机会和威胁:
|
|||||||
|
|
||||||
## 📝 更新日志
|
## 📝 更新日志
|
||||||
|
|
||||||
|
### 2025年11月6日
|
||||||
|
- 🚀 增加了落地页模块
|
||||||
|
|
||||||
### v1.0.0 (2025年10月27日)
|
### v1.0.0 (2025年10月27日)
|
||||||
- ✨ 初始版本发布
|
- ✨ 初始版本发布
|
||||||
|
|
||||||
|
|||||||
8
config.json
Normal file
8
config.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"enableVision": true,
|
||||||
|
"imageConfig": {
|
||||||
|
"maxCount": 4,
|
||||||
|
"maxSizeMB": 4,
|
||||||
|
"allowedTypes": ["image/jpeg", "image/png", "image/webp", "image/gif"]
|
||||||
|
}
|
||||||
|
}
|
||||||
238
css/style.css
238
css/style.css
@@ -350,6 +350,34 @@ iconify-icon {
|
|||||||
max-width: 760px;
|
max-width: 760px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-preview-modal {
|
||||||
|
width: 98vw;
|
||||||
|
height: 98vh;
|
||||||
|
max-width: 98vw;
|
||||||
|
max-height: 98vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-preview-body {
|
||||||
|
flex: 1;
|
||||||
|
background: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-preview-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 3px solid #000;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
.code-viewer {
|
.code-viewer {
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
@@ -461,3 +489,213 @@ iconify-icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#dmermaidSvg{ height: 0px;}
|
#dmermaidSvg{ height: 0px;}
|
||||||
|
|
||||||
|
/* ========== 图片上传相关样式 ========== */
|
||||||
|
|
||||||
|
/* 图片上传按钮 */
|
||||||
|
.image-upload-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn:hover {
|
||||||
|
background: rgba(6, 182, 212, 0.1);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn:disabled:hover {
|
||||||
|
background: transparent;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片缩略图容器 */
|
||||||
|
#image-preview-container {
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 2px dashed #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-preview-container:not(.hidden) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单个图片缩略图项 */
|
||||||
|
.image-thumbnail-item {
|
||||||
|
position: relative;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
background: #f9fafb;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-item:hover {
|
||||||
|
border-color: #06b6d4;
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片删除按钮 */
|
||||||
|
.image-thumbnail-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(220, 38, 38, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-item:hover .image-thumbnail-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-delete:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-delete iconify-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片数量限制提示 */
|
||||||
|
.image-count-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 2px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片预览模态窗 */
|
||||||
|
.image-preview-modal-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-modal-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聊天气泡中的图片显示 */
|
||||||
|
.chat-bubble-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble-image:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片加载中状态 */
|
||||||
|
.image-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-loading::after {
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid #d1d5db;
|
||||||
|
border-top-color: #06b6d4;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传拖拽区域高亮 */
|
||||||
|
.image-drop-active {
|
||||||
|
background: rgba(6, 182, 212, 0.1) !important;
|
||||||
|
border-color: #06b6d4 !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片大小/格式错误提示 */
|
||||||
|
.image-error-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #dc2626;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: toast-in 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
container_name: product-canvas-app
|
container_name: product-canvas-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# 是否启用图片解析功能(Vision API),默认启用
|
||||||
|
- ENABLE_VISION=true
|
||||||
|
#volumes:
|
||||||
|
# 可选:挂载自定义配置文件覆盖默认配置
|
||||||
|
# - ./config.json:/usr/share/nginx/html/config.json:ro
|
||||||
69
index.html
69
index.html
@@ -65,10 +65,19 @@
|
|||||||
|
|
||||||
<!-- 输入区 -->
|
<!-- 输入区 -->
|
||||||
<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 id="image-preview-container" class="mb-2 flex flex-wrap gap-2 hidden">
|
||||||
|
<!-- 动态插入图片缩略图 -->
|
||||||
|
</div>
|
||||||
<div class="relative flex items-center gap-2">
|
<div class="relative flex items-center gap-2">
|
||||||
|
<!-- 图片上传按钮 -->
|
||||||
|
<button id="image-upload-btn" class="image-upload-btn text-gray-500 hover:text-cyan-600 transition-colors p-2" title="上传图片 (支持粘贴)">
|
||||||
|
<iconify-icon icon="ph:image-bold" class="text-2xl"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
<input type="file" id="image-file-input" accept="image/jpeg,image/png,image/webp,image/gif" multiple class="hidden" />
|
||||||
<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>
|
||||||
@@ -76,6 +85,11 @@
|
|||||||
<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 id="image-vision-disabled-tip" class="mt-1 text-xs text-gray-400 hidden">
|
||||||
|
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||||
|
图片解析功能已禁用
|
||||||
|
</div>
|
||||||
<div id="chat-quick-actions" class="mt-2 flex flex-wrap gap-2 hidden"></div>
|
<div id="chat-quick-actions" class="mt-2 flex flex-wrap gap-2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,6 +123,9 @@
|
|||||||
<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="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>
|
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="open-page-btn" class="p-2 bg-cyan-500 text-white border-2 border-black hover:bg-cyan-600 transition-all" title="预览落地页">
|
||||||
|
<iconify-icon icon="ph:monitor-play-bold" 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="查看代码">
|
<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>
|
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
@@ -178,6 +195,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片解析开关 -->
|
||||||
|
<div class="flex items-center justify-between py-3 border-t-2 border-gray-200 mt-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<iconify-icon icon="ph:eye-bold" class="text-lg text-cyan-600"></iconify-icon>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold text-gray-800">启用图片解析</label>
|
||||||
|
<span class="text-xs text-gray-500">允许上传图片并由AI进行视觉理解</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" id="config-enable-vision" class="sr-only peer" checked>
|
||||||
|
<div class="w-11 h-6 bg-gray-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-500"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 状态显示 -->
|
<!-- 状态显示 -->
|
||||||
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
|
<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>
|
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||||
@@ -224,6 +256,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 页面预览模态窗 -->
|
||||||
|
<div id="page-preview-modal" class="modal-overlay">
|
||||||
|
<div class="modal-content page-preview-modal">
|
||||||
|
<div class="bg-gradient-to-r from-teal-500 to-cyan-500 p-3 border-b-4 border-black flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<iconify-icon icon="ph:monitor-play-fill" class="text-2xl text-white"></iconify-icon>
|
||||||
|
<h2 class="text-lg md:text-xl font-black text-white">落地页全屏预览</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button id="page-preview-newtab-btn" class="px-3 py-1 bg-white text-teal-600 border-2 border-black font-semibold hover:bg-teal-50 transition-all flex items-center gap-1">
|
||||||
|
<iconify-icon icon="ph:arrow-square-out-bold" class="text-lg"></iconify-icon>
|
||||||
|
新窗口打开
|
||||||
|
</button>
|
||||||
|
<button id="close-page-preview-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>
|
||||||
|
<div class="page-preview-body">
|
||||||
|
<iframe id="page-preview-iframe" title="落地页全屏预览" sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock" class="page-preview-iframe"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片预览模态窗 -->
|
||||||
|
<div id="image-preview-modal" class="modal-overlay">
|
||||||
|
<div class="image-preview-modal-content">
|
||||||
|
<button id="close-image-preview-btn" class="absolute top-4 right-4 text-white hover:text-gray-300 transition-colors z-10">
|
||||||
|
<iconify-icon icon="ph:x-bold" class="text-3xl"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
<img id="image-preview-full" src="" alt="图片预览" class="max-w-full max-h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 引入JavaScript文件 -->
|
<!-- 引入JavaScript文件 -->
|
||||||
<script src="js/utils.js"></script>
|
<script src="js/utils.js"></script>
|
||||||
<script src="js/services/storage-service.js"></script>
|
<script src="js/services/storage-service.js"></script>
|
||||||
@@ -231,6 +297,7 @@
|
|||||||
<script src="js/core/module-registry.js"></script>
|
<script src="js/core/module-registry.js"></script>
|
||||||
<script src="js/modules/product-canvas.js"></script>
|
<script src="js/modules/product-canvas.js"></script>
|
||||||
<script src="js/modules/swot.js"></script>
|
<script src="js/modules/swot.js"></script>
|
||||||
|
<script src="js/modules/thinksvg.js"></script>
|
||||||
<script src="js/modules/echarts.js"></script>
|
<script src="js/modules/echarts.js"></script>
|
||||||
<script src="js/modules/mermaid.js"></script>
|
<script src="js/modules/mermaid.js"></script>
|
||||||
<script src="js/modules/onepage.js"></script>
|
<script src="js/modules/onepage.js"></script>
|
||||||
|
|||||||
130
js/apiclient.js
130
js/apiclient.js
@@ -2,17 +2,28 @@
|
|||||||
* API客户端 - 处理与AI服务的交互
|
* API客户端 - 处理与AI服务的交互
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 图片上传配置常量
|
||||||
|
const IMAGE_CONFIG = {
|
||||||
|
maxCount: 4, // 最大图片数量
|
||||||
|
maxSizeBytes: 4 * 1024 * 1024, // 单张最大4MB
|
||||||
|
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
|
||||||
|
allowedExtensions: ['.jpg', '.jpeg', '.png', '.webp', '.gif']
|
||||||
|
};
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = {
|
this.config = {
|
||||||
url: '',
|
url: '',
|
||||||
key: '',
|
key: '',
|
||||||
model: ''
|
model: '',
|
||||||
|
enableVision: true // 默认启用图片解析
|
||||||
};
|
};
|
||||||
|
this.runtimeConfig = null; // 运行时配置(从config.json加载)
|
||||||
this.promptMap = {};
|
this.promptMap = {};
|
||||||
this.promptFiles = {
|
this.promptFiles = {
|
||||||
canvas: 'prompts/canvas-prompt.txt',
|
canvas: 'prompts/canvas-prompt.txt',
|
||||||
swot: 'prompts/swot-prompt.txt',
|
swot: 'prompts/swot-prompt.txt',
|
||||||
|
thinksvg: 'prompts/thinksvg-prompt.txt',
|
||||||
echarts: 'prompts/echarts-prompt.txt',
|
echarts: 'prompts/echarts-prompt.txt',
|
||||||
mermaid: 'prompts/mermaid-prompt.txt',
|
mermaid: 'prompts/mermaid-prompt.txt',
|
||||||
onepage: 'prompts/onepage-prompt.txt'
|
onepage: 'prompts/onepage-prompt.txt'
|
||||||
@@ -20,6 +31,7 @@ class APIClient {
|
|||||||
this.promptFallbacks = {
|
this.promptFallbacks = {
|
||||||
canvas: '你是一个专业的产品战略分析师,擅长创建产品画布。',
|
canvas: '你是一个专业的产品战略分析师,擅长创建产品画布。',
|
||||||
swot: '你是一个专业的商业战略分析师,擅长进行SWOT分析。',
|
swot: '你是一个专业的商业战略分析师,擅长进行SWOT分析。',
|
||||||
|
thinksvg: '你是一名思维导图专家,擅长使用 SVG 生成清晰的思维导图。',
|
||||||
echarts:
|
echarts:
|
||||||
'你是一个资深的数据可视化专家,精通将自然语言需求转化为 ECharts 配置对象,请输出结构化 JSON option。',
|
'你是一个资深的数据可视化专家,精通将自然语言需求转化为 ECharts 配置对象,请输出结构化 JSON option。',
|
||||||
mermaid:
|
mermaid:
|
||||||
@@ -30,15 +42,72 @@ class APIClient {
|
|||||||
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
|
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
|
||||||
};
|
};
|
||||||
this.loadConfig();
|
this.loadConfig();
|
||||||
|
this.loadRuntimeConfig(); // 加载运行时配置
|
||||||
this.preloadPrompts(Object.keys(this.promptFiles));
|
this.preloadPrompts(Object.keys(this.promptFiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载运行时配置(从config.json,支持Docker环境变量注入)
|
||||||
|
async loadRuntimeConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('config.json');
|
||||||
|
if (response.ok) {
|
||||||
|
this.runtimeConfig = await response.json();
|
||||||
|
// 运行时配置优先级高于本地存储
|
||||||
|
if (this.runtimeConfig.enableVision !== undefined) {
|
||||||
|
this.config.enableVision = this.runtimeConfig.enableVision;
|
||||||
|
}
|
||||||
|
console.log('运行时配置已加载:', this.runtimeConfig);
|
||||||
|
|
||||||
|
// 触发配置更新事件,通知UI同步
|
||||||
|
this.notifyConfigUpdated();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// config.json不存在时静默失败,使用默认配置
|
||||||
|
console.log('未找到运行时配置文件,使用默认配置');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发配置更新事件
|
||||||
|
notifyConfigUpdated() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent('vision-config-updated', {
|
||||||
|
detail: {
|
||||||
|
enableVision: this.isVisionEnabled(),
|
||||||
|
isRuntimeLocked: this.runtimeConfig && this.runtimeConfig.enableVision !== undefined
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Vision配置是否被运行时锁定
|
||||||
|
isVisionConfigLocked() {
|
||||||
|
return this.runtimeConfig && this.runtimeConfig.enableVision !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// 加载API配置
|
// 加载API配置
|
||||||
loadConfig() {
|
loadConfig() {
|
||||||
const savedConfig = Utils.storage.get('apiConfig');
|
const savedConfig = Utils.storage.get('apiConfig');
|
||||||
if (savedConfig) {
|
if (savedConfig) {
|
||||||
this.config = { ...this.config, ...savedConfig };
|
this.config = { ...this.config, ...savedConfig };
|
||||||
}
|
}
|
||||||
|
// 确保enableVision有默认值
|
||||||
|
if (this.config.enableVision === undefined) {
|
||||||
|
this.config.enableVision = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查图片解析是否启用
|
||||||
|
isVisionEnabled() {
|
||||||
|
// 运行时配置优先级最高
|
||||||
|
if (this.runtimeConfig && this.runtimeConfig.enableVision !== undefined) {
|
||||||
|
return this.runtimeConfig.enableVision;
|
||||||
|
}
|
||||||
|
return this.config.enableVision !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片配置
|
||||||
|
getImageConfig() {
|
||||||
|
return { ...IMAGE_CONFIG };
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadPrompts(keys = []) {
|
preloadPrompts(keys = []) {
|
||||||
@@ -116,29 +185,71 @@ class APIClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildMessagesForModule(manifest, userMessage, contextMessages = []) {
|
async buildMessagesForModule(manifest, userMessage, contextMessages = [], images = []) {
|
||||||
const prompt =
|
const prompt =
|
||||||
(manifest && manifest.promptKey
|
(manifest && manifest.promptKey
|
||||||
? await this.ensurePrompt(manifest.promptKey)
|
? await this.ensurePrompt(manifest.promptKey)
|
||||||
: null) || this.promptFallbacks.default;
|
: null) || this.promptFallbacks.default;
|
||||||
|
|
||||||
|
// 构建用户消息内容(支持图片)
|
||||||
|
const userContent = this.buildUserContent(userMessage, images);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ role: 'system', content: prompt },
|
{ role: 'system', content: prompt },
|
||||||
...contextMessages,
|
...contextMessages,
|
||||||
{ role: 'user', content: userMessage }
|
{ role: 'user', content: userContent }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户消息内容(支持图片的OpenAI Vision API格式)
|
||||||
|
* @param {string} text - 文本内容
|
||||||
|
* @param {Array} images - 图片数组,每项包含 { base64, mimeType }
|
||||||
|
* @returns {string|Array} - 纯文本或多模态内容数组
|
||||||
|
*/
|
||||||
|
buildUserContent(text, images = []) {
|
||||||
|
// 如果没有图片或未启用Vision,返回纯文本
|
||||||
|
if (!images || images.length === 0 || !this.isVisionEnabled()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建多模态内容数组(OpenAI Vision API格式)
|
||||||
|
const content = [];
|
||||||
|
|
||||||
|
// 添加图片
|
||||||
|
for (const img of images) {
|
||||||
|
content.push({
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: `data:${img.mimeType};base64,${img.base64}`,
|
||||||
|
detail: 'auto' // 可选: 'low', 'high', 'auto'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文本(放在图片后面)
|
||||||
|
if (text && text.trim()) {
|
||||||
|
content.push({
|
||||||
|
type: 'text',
|
||||||
|
text: text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
async generateModuleCompletion(
|
async generateModuleCompletion(
|
||||||
manifest,
|
manifest,
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages = [],
|
contextMessages = [],
|
||||||
options = {}
|
options = {},
|
||||||
|
images = []
|
||||||
) {
|
) {
|
||||||
const messages = await this.buildMessagesForModule(
|
const messages = await this.buildMessagesForModule(
|
||||||
manifest,
|
manifest,
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages
|
contextMessages,
|
||||||
|
images
|
||||||
);
|
);
|
||||||
return this.sendChatMessage(messages, options);
|
return this.sendChatMessage(messages, options);
|
||||||
}
|
}
|
||||||
@@ -149,12 +260,14 @@ class APIClient {
|
|||||||
contextMessages = [],
|
contextMessages = [],
|
||||||
onChunk,
|
onChunk,
|
||||||
onComplete,
|
onComplete,
|
||||||
options = {}
|
options = {},
|
||||||
|
images = []
|
||||||
) {
|
) {
|
||||||
const messages = await this.buildMessagesForModule(
|
const messages = await this.buildMessagesForModule(
|
||||||
manifest,
|
manifest,
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages
|
contextMessages,
|
||||||
|
images
|
||||||
);
|
);
|
||||||
return this.sendChatMessageStream(messages, options, onChunk, onComplete);
|
return this.sendChatMessageStream(messages, options, onChunk, onComplete);
|
||||||
}
|
}
|
||||||
@@ -298,7 +411,8 @@ class APIClient {
|
|||||||
.filter(msg => msg.id <= messageId)
|
.filter(msg => msg.id <= messageId)
|
||||||
.map(msg => ({
|
.map(msg => ({
|
||||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
// 优先使用原始内容,保证上下文完整性
|
||||||
|
content: msg.rawContent || msg.content || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (contextMessages.length === 0) {
|
if (contextMessages.length === 0) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const STREAM_DEFAULT_OPTIONS = {
|
const STREAM_DEFAULT_OPTIONS = {
|
||||||
maxTokens: 13000,
|
maxTokens: 30000,
|
||||||
temperature: 0.7
|
temperature: 0.7
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@
|
|||||||
this.mermaidPanZoom = null;
|
this.mermaidPanZoom = null;
|
||||||
this.mermaidInitialized = false;
|
this.mermaidInitialized = false;
|
||||||
|
|
||||||
|
// 图片上传状态管理
|
||||||
|
this.pendingImages = []; // 待发送的图片列表 { id, file, blobUrl, base64?, mimeType }
|
||||||
|
|
||||||
this.globalStore = moduleRuntime.storageService.global();
|
this.globalStore = moduleRuntime.storageService.global();
|
||||||
this.activeModuleId = null;
|
this.activeModuleId = null;
|
||||||
|
|
||||||
@@ -57,6 +60,16 @@
|
|||||||
this.el.clearHistoryBtn = document.getElementById('clear-history-btn');
|
this.el.clearHistoryBtn = document.getElementById('clear-history-btn');
|
||||||
this.el.chatHistory = document.getElementById('chat-history');
|
this.el.chatHistory = document.getElementById('chat-history');
|
||||||
|
|
||||||
|
// 图片上传相关
|
||||||
|
this.el.imageUploadBtn = document.getElementById('image-upload-btn');
|
||||||
|
this.el.imageFileInput = document.getElementById('image-file-input');
|
||||||
|
this.el.imagePreviewContainer = document.getElementById('image-preview-container');
|
||||||
|
this.el.imagePreviewModal = document.getElementById('image-preview-modal');
|
||||||
|
this.el.imagePreviewFull = document.getElementById('image-preview-full');
|
||||||
|
this.el.closeImagePreviewBtn = document.getElementById('close-image-preview-btn');
|
||||||
|
this.el.imageVisionDisabledTip = document.getElementById('image-vision-disabled-tip');
|
||||||
|
this.el.configEnableVision = document.getElementById('config-enable-vision');
|
||||||
|
|
||||||
// 视图区域
|
// 视图区域
|
||||||
this.el.viewer = document.getElementById('svg-viewer');
|
this.el.viewer = document.getElementById('svg-viewer');
|
||||||
this.el.placeholderText =
|
this.el.placeholderText =
|
||||||
@@ -69,6 +82,7 @@
|
|||||||
this.el.downloadSvgBtn = document.getElementById('download-svg-btn');
|
this.el.downloadSvgBtn = document.getElementById('download-svg-btn');
|
||||||
this.el.copyImageBtn = document.getElementById('copy-image-btn');
|
this.el.copyImageBtn = document.getElementById('copy-image-btn');
|
||||||
this.el.exportImageBtn = document.getElementById('export-image-btn');
|
this.el.exportImageBtn = document.getElementById('export-image-btn');
|
||||||
|
this.el.openPageBtn = document.getElementById('open-page-btn');
|
||||||
this.el.viewCodeBtn = document.getElementById('view-code-btn');
|
this.el.viewCodeBtn = document.getElementById('view-code-btn');
|
||||||
const toolbarContainer = this.el.viewCodeBtn?.parentElement;
|
const toolbarContainer = this.el.viewCodeBtn?.parentElement;
|
||||||
if (toolbarContainer) {
|
if (toolbarContainer) {
|
||||||
@@ -104,6 +118,14 @@
|
|||||||
this.el.codeContent = document.getElementById('code-content');
|
this.el.codeContent = document.getElementById('code-content');
|
||||||
this.el.copyCodeBtn = document.getElementById('copy-code-btn');
|
this.el.copyCodeBtn = document.getElementById('copy-code-btn');
|
||||||
this.el.closeCodeModalBtn = document.getElementById('close-code-modal-btn');
|
this.el.closeCodeModalBtn = document.getElementById('close-code-modal-btn');
|
||||||
|
this.el.pagePreviewModal = document.getElementById('page-preview-modal');
|
||||||
|
this.el.pagePreviewIframe = document.getElementById('page-preview-iframe');
|
||||||
|
this.el.closePagePreviewBtn = document.getElementById(
|
||||||
|
'close-page-preview-btn'
|
||||||
|
);
|
||||||
|
this.el.pagePreviewNewTabBtn = document.getElementById(
|
||||||
|
'page-preview-newtab-btn'
|
||||||
|
);
|
||||||
|
|
||||||
// 复制按钮可用性
|
// 复制按钮可用性
|
||||||
if (this.el.copyImageBtn && !this.copyClipboardSupported) {
|
if (this.el.copyImageBtn && !this.copyClipboardSupported) {
|
||||||
@@ -193,6 +215,11 @@
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (this.el.openPageBtn) {
|
||||||
|
this.el.openPageBtn.addEventListener('click', () =>
|
||||||
|
this.openPagePreview()
|
||||||
|
);
|
||||||
|
}
|
||||||
if (this.el.viewCodeBtn) {
|
if (this.el.viewCodeBtn) {
|
||||||
this.el.viewCodeBtn.addEventListener('click', () =>
|
this.el.viewCodeBtn.addEventListener('click', () =>
|
||||||
this.viewArtifactCode()
|
this.viewArtifactCode()
|
||||||
@@ -249,6 +276,23 @@
|
|||||||
this.closeCodeModal()
|
this.closeCodeModal()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (this.el.closePagePreviewBtn) {
|
||||||
|
this.el.closePagePreviewBtn.addEventListener('click', () =>
|
||||||
|
this.closePagePreview()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.el.pagePreviewModal) {
|
||||||
|
this.el.pagePreviewModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === this.el.pagePreviewModal) {
|
||||||
|
this.closePagePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.el.pagePreviewNewTabBtn) {
|
||||||
|
this.el.pagePreviewNewTabBtn.addEventListener('click', () =>
|
||||||
|
this.openPageInNewTab()
|
||||||
|
);
|
||||||
|
}
|
||||||
if (this.el.codeModal) {
|
if (this.el.codeModal) {
|
||||||
this.el.codeModal.addEventListener('click', (event) => {
|
this.el.codeModal.addEventListener('click', (event) => {
|
||||||
if (event.target === this.el.codeModal) {
|
if (event.target === this.el.codeModal) {
|
||||||
@@ -256,6 +300,101 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 图片上传相关事件绑定
|
||||||
|
this.bindImageUploadEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定图片上传相关事件
|
||||||
|
*/
|
||||||
|
bindImageUploadEvents() {
|
||||||
|
// 图片上传按钮点击
|
||||||
|
if (this.el.imageUploadBtn && this.el.imageFileInput) {
|
||||||
|
this.el.imageUploadBtn.addEventListener('click', () => {
|
||||||
|
if (!this.apiClient.isVisionEnabled()) {
|
||||||
|
this.showImageError('图片解析功能已禁用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.el.imageFileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件选择变化
|
||||||
|
if (this.el.imageFileInput) {
|
||||||
|
this.el.imageFileInput.addEventListener('change', (event) => {
|
||||||
|
const files = Array.from(event.target.files || []);
|
||||||
|
this.handleImageFiles(files);
|
||||||
|
event.target.value = ''; // 清空以允许重复选择同一文件
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 粘贴事件(支持图片粘贴)
|
||||||
|
if (this.el.chatInput) {
|
||||||
|
this.el.chatInput.addEventListener('paste', (event) => {
|
||||||
|
if (!this.apiClient.isVisionEnabled()) return;
|
||||||
|
|
||||||
|
const items = event.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
const imageFiles = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type.startsWith('image/')) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) imageFiles.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
// 不阻止默认行为,允许同时粘贴文本
|
||||||
|
this.handleImageFiles(imageFiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览容器点击事件(删除和预览)
|
||||||
|
if (this.el.imagePreviewContainer) {
|
||||||
|
this.el.imagePreviewContainer.addEventListener('click', (event) => {
|
||||||
|
const deleteBtn = event.target.closest('.image-thumbnail-delete');
|
||||||
|
if (deleteBtn) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const imageId = deleteBtn.dataset.imageId;
|
||||||
|
this.removeImage(imageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailItem = event.target.closest('.image-thumbnail-item');
|
||||||
|
if (thumbnailItem) {
|
||||||
|
const imageId = thumbnailItem.dataset.imageId;
|
||||||
|
this.openImagePreview(imageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览模态窗关闭
|
||||||
|
if (this.el.closeImagePreviewBtn) {
|
||||||
|
this.el.closeImagePreviewBtn.addEventListener('click', () => {
|
||||||
|
this.closeImagePreviewModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.el.imagePreviewModal) {
|
||||||
|
this.el.imagePreviewModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === this.el.imagePreviewModal) {
|
||||||
|
this.closeImagePreviewModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化图片上传UI状态
|
||||||
|
this.updateImageUploadUI();
|
||||||
|
|
||||||
|
// 监听运行时配置更新事件
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('vision-config-updated', (event) => {
|
||||||
|
this.updateImageUploadUI();
|
||||||
|
this.updateVisionConfigUI(event.detail);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupModuleSwitcher() {
|
setupModuleSwitcher() {
|
||||||
@@ -337,6 +476,10 @@
|
|||||||
}
|
}
|
||||||
this.renderQuickActions(manifest.ui?.quickActions || []);
|
this.renderQuickActions(manifest.ui?.quickActions || []);
|
||||||
this.showViewerPlaceholder(manifest.ui?.placeholderText || '');
|
this.showViewerPlaceholder(manifest.ui?.placeholderText || '');
|
||||||
|
if (this.el.openPageBtn) {
|
||||||
|
const isHtmlModule = manifest.artifact?.type === 'html';
|
||||||
|
this.el.openPageBtn.classList.toggle('hidden', !isHtmlModule);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showViewerPlaceholder(text) {
|
showViewerPlaceholder(text) {
|
||||||
@@ -426,14 +569,21 @@
|
|||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
const actionsHtml = this.buildMessageActions(message, options);
|
const actionsHtml = this.buildMessageActions(message, options);
|
||||||
|
|
||||||
|
// 构建图片HTML(用于用户消息)
|
||||||
|
const imagesHtml = this.buildMessageImagesHtml(message.images);
|
||||||
|
|
||||||
if (message.type === 'user') {
|
if (message.type === 'user') {
|
||||||
wrapper.className = 'flex justify-end';
|
wrapper.className = 'flex justify-end';
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="chat-bubble-user message-with-delete" data-message-id="${message.id}">
|
<div class="chat-bubble-user message-with-delete" data-message-id="${message.id}">
|
||||||
<div>${Utils.escapeHtml(message.content)}</div>
|
${imagesHtml}
|
||||||
|
<div>${Utils.escapeHtml(message.content || '')}</div>
|
||||||
${actionsHtml}
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// 绑定图片点击预览事件
|
||||||
|
this.bindBubbleImagePreview(wrapper, message.images);
|
||||||
} else if (message.type === 'error') {
|
} else if (message.type === 'error') {
|
||||||
wrapper.className = 'flex justify-start';
|
wrapper.className = 'flex justify-start';
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
@@ -474,6 +624,47 @@
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建消息中图片的HTML
|
||||||
|
* @param {Array} images - 图片数组
|
||||||
|
* @returns {string} - HTML字符串
|
||||||
|
*/
|
||||||
|
buildMessageImagesHtml(images) {
|
||||||
|
if (!images || images.length === 0) return '';
|
||||||
|
|
||||||
|
const imageItems = images.map((img, index) => {
|
||||||
|
const src = `data:${img.mimeType};base64,${img.base64}`;
|
||||||
|
return `
|
||||||
|
<div class="chat-bubble-image" data-image-index="${index}">
|
||||||
|
<img src="${src}" alt="图片 ${index + 1}" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<div class="chat-bubble-images">${imageItems}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定气泡中图片的点击预览事件
|
||||||
|
* @param {HTMLElement} wrapper - 消息包装元素
|
||||||
|
* @param {Array} images - 图片数组
|
||||||
|
*/
|
||||||
|
bindBubbleImagePreview(wrapper, images) {
|
||||||
|
if (!images || images.length === 0) return;
|
||||||
|
|
||||||
|
const imageElements = wrapper.querySelectorAll('.chat-bubble-image');
|
||||||
|
imageElements.forEach((el, index) => {
|
||||||
|
el.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const img = images[index];
|
||||||
|
if (img && this.el.imagePreviewModal && this.el.imagePreviewFull) {
|
||||||
|
this.el.imagePreviewFull.src = `data:${img.mimeType};base64,${img.base64}`;
|
||||||
|
this.el.imagePreviewModal.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
buildMessageActions(message, options = {}) {
|
buildMessageActions(message, options = {}) {
|
||||||
const {
|
const {
|
||||||
allowRollback = false,
|
allowRollback = false,
|
||||||
@@ -656,7 +847,9 @@
|
|||||||
.filter((msg) => msg.type === 'user' || msg.type === 'ai')
|
.filter((msg) => msg.type === 'user' || msg.type === 'ai')
|
||||||
.map((msg) => ({
|
.map((msg) => ({
|
||||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
// 使用原始内容作为 LLM 上下文,兼容无 rawContent 的旧记录
|
||||||
|
content: msg.rawContent || msg.content || '',
|
||||||
|
images: msg.type === 'user' ? msg.images || [] : undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
@@ -666,7 +859,8 @@
|
|||||||
|
|
||||||
this.beginStreaming(manifest, {
|
this.beginStreaming(manifest, {
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages
|
contextMessages,
|
||||||
|
images: userMessage.images || [] // 传递原始用户消息的图片
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,10 +903,14 @@
|
|||||||
return nextId;
|
return nextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage() {
|
async sendMessage() {
|
||||||
if (!this.el.chatInput) return;
|
if (!this.el.chatInput) return;
|
||||||
const message = this.el.chatInput.value.trim();
|
const message = this.el.chatInput.value.trim();
|
||||||
if (!message || this.isProcessing) return;
|
const hasImages = this.pendingImages.length > 0;
|
||||||
|
|
||||||
|
// 必须有文本或图片才能发送
|
||||||
|
if (!message && !hasImages) return;
|
||||||
|
if (this.isProcessing) return;
|
||||||
|
|
||||||
if (!this.apiClient.isConfigValid()) {
|
if (!this.apiClient.isConfigValid()) {
|
||||||
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
|
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
|
||||||
@@ -720,12 +918,25 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 准备图片数据(转换为base64)
|
||||||
|
let images = [];
|
||||||
|
if (hasImages && this.apiClient.isVisionEnabled()) {
|
||||||
|
try {
|
||||||
|
images = await this.prepareImagesForSend();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片处理失败:', error);
|
||||||
|
this.showImageError('图片处理失败,请重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
id: Utils.generateId('msg'),
|
id: Utils.generateId('msg'),
|
||||||
type: 'user',
|
type: 'user',
|
||||||
content: message,
|
content: message,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
images: images.length > 0 ? images : undefined // 保存图片数据到消息
|
||||||
};
|
};
|
||||||
|
|
||||||
this.conversationService.appendMessage(manifest, userMessage);
|
this.conversationService.appendMessage(manifest, userMessage);
|
||||||
@@ -733,6 +944,9 @@
|
|||||||
this.el.chatInput.value = '';
|
this.el.chatInput.value = '';
|
||||||
Utils.autoResizeTextarea(this.el.chatInput);
|
Utils.autoResizeTextarea(this.el.chatInput);
|
||||||
|
|
||||||
|
// 清空待发送图片
|
||||||
|
this.clearPendingImages();
|
||||||
|
|
||||||
const context = this.conversationService.buildContext(manifest);
|
const context = this.conversationService.buildContext(manifest);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
console.warn('无法构建上下文,终止发送');
|
console.warn('无法构建上下文,终止发送');
|
||||||
@@ -746,7 +960,8 @@
|
|||||||
|
|
||||||
this.beginStreaming(manifest, {
|
this.beginStreaming(manifest, {
|
||||||
userMessage: context.userMessage,
|
userMessage: context.userMessage,
|
||||||
contextMessages: context.contextMessages
|
contextMessages: context.contextMessages,
|
||||||
|
images: images // 传递图片数据
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,7 +1016,7 @@
|
|||||||
const delta = chunk?.choices?.[0]?.delta?.content || '';
|
const delta = chunk?.choices?.[0]?.delta?.content || '';
|
||||||
if (!delta) return;
|
if (!delta) return;
|
||||||
fullContent += delta;
|
fullContent += delta;
|
||||||
this.updateStreamingContent(container, fullContent);
|
this.updateStreamingContent(container, fullContent, streamState, manifest);
|
||||||
if (manifest.artifact?.type === 'svg') {
|
if (manifest.artifact?.type === 'svg') {
|
||||||
this.processSvgStreamChunk(manifest, fullContent, streamState);
|
this.processSvgStreamChunk(manifest, fullContent, streamState);
|
||||||
} else if (manifest.artifact?.type === 'mermaid') {
|
} else if (manifest.artifact?.type === 'mermaid') {
|
||||||
@@ -822,7 +1037,8 @@
|
|||||||
payload.contextMessages,
|
payload.contextMessages,
|
||||||
handleChunk,
|
handleChunk,
|
||||||
handleComplete,
|
handleComplete,
|
||||||
STREAM_DEFAULT_OPTIONS
|
STREAM_DEFAULT_OPTIONS,
|
||||||
|
payload.images || [] // 传递图片数据
|
||||||
)
|
)
|
||||||
.then((streamHandle) => {
|
.then((streamHandle) => {
|
||||||
this.activeStreamHandle = streamHandle;
|
this.activeStreamHandle = streamHandle;
|
||||||
@@ -858,13 +1074,21 @@
|
|||||||
return messageDiv;
|
return messageDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStreamingContent(container, content) {
|
updateStreamingContent(container, content, streamContext = null, manifest = null) {
|
||||||
const cursor = container.querySelector('.typing-cursor');
|
const cursor = container.querySelector('.typing-cursor');
|
||||||
if (!cursor) return;
|
if (!cursor) return;
|
||||||
|
|
||||||
|
// 对于 HTML 模块,HTML 内容已经在右侧预览区域实时渲染,
|
||||||
|
// 为避免在对话气泡中直接注入 <html>/<body> 文档结构,这里不再渲染流式内容。
|
||||||
|
if (manifest?.artifact?.type === 'html') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayContent = content;
|
||||||
if (typeof marked !== 'undefined') {
|
if (typeof marked !== 'undefined') {
|
||||||
cursor.innerHTML = marked.parse(content);
|
cursor.innerHTML = marked.parse(displayContent);
|
||||||
} else {
|
} else {
|
||||||
cursor.textContent = content;
|
cursor.textContent = displayContent;
|
||||||
}
|
}
|
||||||
Utils.scrollToBottom(this.el.chatHistory);
|
Utils.scrollToBottom(this.el.chatHistory);
|
||||||
}
|
}
|
||||||
@@ -959,7 +1183,10 @@
|
|||||||
const messageRecord = {
|
const messageRecord = {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
type: 'ai',
|
type: 'ai',
|
||||||
|
// content 用于展示(可能是裁剪/清洗后的文本)
|
||||||
content: messageContent,
|
content: messageContent,
|
||||||
|
// rawContent 保留完整的 LLM 原始响应,用于上下文与重新生成
|
||||||
|
rawContent: fullContent,
|
||||||
timestamp,
|
timestamp,
|
||||||
artifactId
|
artifactId
|
||||||
};
|
};
|
||||||
@@ -992,6 +1219,7 @@
|
|||||||
type: 'ai',
|
type: 'ai',
|
||||||
content:
|
content:
|
||||||
content || '生成已被手动终止,您可以点击重新生成继续。',
|
content || '生成已被手动终止,您可以点击重新生成继续。',
|
||||||
|
rawContent: fullContent || content || '',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
artifactId: null,
|
artifactId: null,
|
||||||
interrupted: true
|
interrupted: true
|
||||||
@@ -1049,7 +1277,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = segments.filter(Boolean).join('\n\n').trim();
|
let content = segments.filter(Boolean).join('\n\n').trim();
|
||||||
|
|
||||||
|
// 对于 HTML 模块,额外清理尾部可能残留的空 ```html 代码块
|
||||||
|
// 场景:模型输出两个 ```html 代码块,第二个为空;解析器只提取第一个,
|
||||||
|
// 剩余的空代码块会进入 before/after,最终在气泡中渲染出一个空的 <pre><code>。
|
||||||
|
if (manifest.artifact?.type === 'html' && content) {
|
||||||
|
const withoutEmptyHtmlFence = content.replace(
|
||||||
|
/\n*```(?:html|htm)?\s*```[\s]*$/i,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
if (withoutEmptyHtmlFence.trim()) {
|
||||||
|
content = withoutEmptyHtmlFence.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@@ -1058,6 +1300,15 @@
|
|||||||
return `已生成 ${manifest.label} 图表,请点击占位卡片查看。`;
|
return `已生成 ${manifest.label} 图表,请点击占位卡片查看。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTML 模块:避免将完整 HTML 文档渲染到对话气泡中
|
||||||
|
if (manifest.artifact?.type === 'html') {
|
||||||
|
const safeText = (rawContent || '')
|
||||||
|
// 去掉主/副 ```html 代码块,保留其余说明文字
|
||||||
|
.replace(/```(?:html|htm)?[\s\S]*?```/gi, '')
|
||||||
|
.trim();
|
||||||
|
return safeText || `已生成 ${manifest.label} 页面内容,请查看预览区域。`;
|
||||||
|
}
|
||||||
|
|
||||||
return rawContent.trim();
|
return rawContent.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1074,8 +1325,9 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const svgCtx = streamState.svg;
|
const svgCtx = streamState.svg;
|
||||||
|
// 兼容 ```svg、```xml、``` xml、``` svg 等格式
|
||||||
const startPattern =
|
const startPattern =
|
||||||
manifest.artifact?.startPattern || /```(?:svg)?\s*<svg/i;
|
manifest.artifact?.startPattern || /```\s*(?:svg|xml)?\s*<svg/i;
|
||||||
if (!svgCtx.started) {
|
if (!svgCtx.started) {
|
||||||
const match = fullContent.match(startPattern);
|
const match = fullContent.match(startPattern);
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -1096,7 +1348,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const svgSection = fullContent.substring(svgCtx.startIndex);
|
const svgSection = fullContent.substring(svgCtx.startIndex);
|
||||||
let cleaned = svgSection.replace(/```(?:svg)?\s*/i, '');
|
// 兼容清理 ```svg、```xml、``` xml、``` svg 等前缀
|
||||||
|
let cleaned = svgSection.replace(/```\s*(?:svg|xml)?\s*/i, '');
|
||||||
cleaned = cleaned.replace(/```$/, '');
|
cleaned = cleaned.replace(/```$/, '');
|
||||||
const closingIndex = cleaned.indexOf('</svg>');
|
const closingIndex = cleaned.indexOf('</svg>');
|
||||||
if (closingIndex !== -1) {
|
if (closingIndex !== -1) {
|
||||||
@@ -1956,6 +2209,79 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActiveHtmlArtifact(manifest = null) {
|
||||||
|
const targetManifest = manifest || this.getActiveManifest();
|
||||||
|
if (!targetManifest || targetManifest.artifact?.type !== 'html') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const state = this.runtime.getState(targetManifest.id);
|
||||||
|
if (!state?.currentArtifactId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return state.artifacts[state.currentArtifactId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
openPagePreview() {
|
||||||
|
const manifest = this.getActiveManifest();
|
||||||
|
if (!manifest || manifest.artifact?.type !== 'html') {
|
||||||
|
alert('当前模块不支持页面预览,请切换到落地页生成模块。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const artifact = this.getActiveHtmlArtifact(manifest);
|
||||||
|
if (!artifact || !artifact.content) {
|
||||||
|
alert('尚未生成落地页内容,请先完成一次生成。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const htmlDoc = this.prepareHtmlDocument(artifact.content, {
|
||||||
|
partial: false
|
||||||
|
});
|
||||||
|
if (this.el.pagePreviewIframe) {
|
||||||
|
this.el.pagePreviewIframe.srcdoc = htmlDoc;
|
||||||
|
this.el.pagePreviewIframe.dataset.updatedAt =
|
||||||
|
new Date().toISOString();
|
||||||
|
}
|
||||||
|
if (this.el.pagePreviewModal) {
|
||||||
|
this.el.pagePreviewModal.classList.add('active');
|
||||||
|
this.el.pagePreviewModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closePagePreview() {
|
||||||
|
if (!this.el.pagePreviewModal) return;
|
||||||
|
this.el.pagePreviewModal.classList.remove('active');
|
||||||
|
this.el.pagePreviewModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
openPageInNewTab() {
|
||||||
|
const manifest = this.getActiveManifest();
|
||||||
|
if (!manifest || manifest.artifact?.type !== 'html') {
|
||||||
|
alert('只能在落地页生成模块中打开新窗口预览。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const artifact = this.getActiveHtmlArtifact(manifest);
|
||||||
|
if (!artifact || !artifact.content) {
|
||||||
|
alert('当前落地页内容为空,请先生成或刷新内容。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const htmlDoc = this.prepareHtmlDocument(artifact.content, {
|
||||||
|
partial: false
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const blob = new Blob([htmlDoc], { type: 'text/html' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const opened = window.open(url, '_blank', 'noopener');
|
||||||
|
if (!opened) {
|
||||||
|
//alert('浏览器阻止了新窗口,请允许弹窗或手动复制代码。');
|
||||||
|
} else if (opened.focus) {
|
||||||
|
opened.focus();
|
||||||
|
}
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开新窗口失败:', error);
|
||||||
|
alert('打开新窗口失败,请检查浏览器设置或下载代码后手动打开。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
cancelActiveStream() {
|
cancelActiveStream() {
|
||||||
this.manualAbortRequested = true;
|
this.manualAbortRequested = true;
|
||||||
@@ -2113,6 +2439,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.el.viewer.appendChild(wrapper);
|
this.el.viewer.appendChild(wrapper);
|
||||||
|
|
||||||
|
if (this.el.pagePreviewIframe && !partial) {
|
||||||
|
this.el.pagePreviewIframe.srcdoc = preparedHtml;
|
||||||
|
this.el.pagePreviewIframe.dataset.updatedAt =
|
||||||
|
new Date().toISOString();
|
||||||
|
} else if (this.el.pagePreviewIframe && partial) {
|
||||||
|
this.el.pagePreviewIframe.srcdoc = preparedHtml;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareHtmlDocument(htmlContent, options = {}) {
|
prepareHtmlDocument(htmlContent, options = {}) {
|
||||||
@@ -2167,13 +2501,6 @@
|
|||||||
|
|
||||||
ensureClosingTag('html', () => output.length);
|
ensureClosingTag('html', () => output.length);
|
||||||
|
|
||||||
if (partial) {
|
|
||||||
// 对流式场景追加最外层闭合,避免 iframe 在标签未闭合时渲染失败
|
|
||||||
if (!/</i.test(output.slice(-10))) {
|
|
||||||
output += '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2496,6 +2823,10 @@
|
|||||||
this.el.editMermaidBtn.classList.toggle('hidden', !isMermaidManifest);
|
this.el.editMermaidBtn.classList.toggle('hidden', !isMermaidManifest);
|
||||||
this.el.editMermaidBtn.disabled = !hasArtifact || !isMermaidManifest;
|
this.el.editMermaidBtn.disabled = !hasArtifact || !isMermaidManifest;
|
||||||
}
|
}
|
||||||
|
if (this.el.openPageBtn) {
|
||||||
|
const isHtmlManifest = manifest.artifact?.type === 'html';
|
||||||
|
this.el.openPageBtn.disabled = !hasArtifact || !isHtmlManifest;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCurrentConversation() {
|
clearCurrentConversation() {
|
||||||
@@ -2556,6 +2887,11 @@
|
|||||||
if (this.el.apiUrlInput) this.el.apiUrlInput.value = config.url || '';
|
if (this.el.apiUrlInput) this.el.apiUrlInput.value = config.url || '';
|
||||||
if (this.el.apiKeyInput) this.el.apiKeyInput.value = config.key || '';
|
if (this.el.apiKeyInput) this.el.apiKeyInput.value = config.key || '';
|
||||||
if (this.el.apiModelInput) this.el.apiModelInput.value = config.model || '';
|
if (this.el.apiModelInput) this.el.apiModelInput.value = config.model || '';
|
||||||
|
if (this.el.configEnableVision) {
|
||||||
|
this.el.configEnableVision.checked = config.enableVision !== false;
|
||||||
|
}
|
||||||
|
// 更新图片上传UI状态
|
||||||
|
this.updateImageUploadUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
async testAPI() {
|
async testAPI() {
|
||||||
@@ -2572,7 +2908,8 @@
|
|||||||
const config = {
|
const config = {
|
||||||
url: this.el.apiUrlInput?.value.trim(),
|
url: this.el.apiUrlInput?.value.trim(),
|
||||||
key: this.el.apiKeyInput?.value.trim(),
|
key: this.el.apiKeyInput?.value.trim(),
|
||||||
model: this.el.apiModelInput?.value.trim()
|
model: this.el.apiModelInput?.value.trim(),
|
||||||
|
enableVision: this.el.configEnableVision?.checked !== false
|
||||||
};
|
};
|
||||||
if (!config.url || !config.key || !config.model) {
|
if (!config.url || !config.key || !config.model) {
|
||||||
this.setConfigStatus('error', '请填写完整的配置');
|
this.setConfigStatus('error', '请填写完整的配置');
|
||||||
@@ -2580,6 +2917,8 @@
|
|||||||
}
|
}
|
||||||
this.apiClient.saveConfig(config);
|
this.apiClient.saveConfig(config);
|
||||||
this.setConfigStatus('success', '配置已保存');
|
this.setConfigStatus('success', '配置已保存');
|
||||||
|
// 更新图片上传UI状态
|
||||||
|
this.updateImageUploadUI();
|
||||||
//setTimeout(() => this.closeConfigModal(), 600);
|
//setTimeout(() => this.closeConfigModal(), 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2627,6 +2966,255 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 图片上传相关方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理图片文件(上传或粘贴)
|
||||||
|
* @param {File[]} files - 文件列表
|
||||||
|
*/
|
||||||
|
handleImageFiles(files) {
|
||||||
|
const config = this.apiClient.getImageConfig();
|
||||||
|
const currentCount = this.pendingImages.length;
|
||||||
|
const availableSlots = config.maxCount - currentCount;
|
||||||
|
|
||||||
|
if (availableSlots <= 0) {
|
||||||
|
this.showImageError(`最多只能上传 ${config.maxCount} 张图片`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToAdd = files.slice(0, availableSlots);
|
||||||
|
|
||||||
|
for (const file of filesToAdd) {
|
||||||
|
// 验证文件类型
|
||||||
|
if (!config.allowedTypes.includes(file.type)) {
|
||||||
|
this.showImageError(`不支持的图片格式: ${file.type}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小
|
||||||
|
if (file.size > config.maxSizeBytes) {
|
||||||
|
const maxSizeMB = config.maxSizeBytes / (1024 * 1024);
|
||||||
|
this.showImageError(`图片大小超过限制 (最大 ${maxSizeMB}MB)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建图片对象
|
||||||
|
const imageObj = {
|
||||||
|
id: Utils.generateId('img'),
|
||||||
|
file: file,
|
||||||
|
blobUrl: URL.createObjectURL(file),
|
||||||
|
mimeType: file.type,
|
||||||
|
base64: null // 发送时再转换
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pendingImages.push(imageObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderImageThumbnails();
|
||||||
|
|
||||||
|
if (files.length > availableSlots) {
|
||||||
|
this.showImageError(`已达到最大图片数量限制 (${config.maxCount} 张)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染图片缩略图
|
||||||
|
*/
|
||||||
|
renderImageThumbnails() {
|
||||||
|
if (!this.el.imagePreviewContainer) return;
|
||||||
|
|
||||||
|
if (this.pendingImages.length === 0) {
|
||||||
|
this.el.imagePreviewContainer.classList.add('hidden');
|
||||||
|
this.el.imagePreviewContainer.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.el.imagePreviewContainer.classList.remove('hidden');
|
||||||
|
this.el.imagePreviewContainer.innerHTML = this.pendingImages.map((img, index) => `
|
||||||
|
<div class="image-thumbnail-item" data-image-id="${img.id}" title="点击预览">
|
||||||
|
<img src="${img.blobUrl}" alt="图片 ${index + 1}" />
|
||||||
|
<button class="image-thumbnail-delete" data-image-id="${img.id}" title="删除">
|
||||||
|
<iconify-icon icon="ph:x-bold"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定图片
|
||||||
|
* @param {string} imageId - 图片ID
|
||||||
|
*/
|
||||||
|
removeImage(imageId) {
|
||||||
|
const index = this.pendingImages.findIndex(img => img.id === imageId);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const img = this.pendingImages[index];
|
||||||
|
// 释放Blob URL
|
||||||
|
if (img.blobUrl) {
|
||||||
|
URL.revokeObjectURL(img.blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingImages.splice(index, 1);
|
||||||
|
this.renderImageThumbnails();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有待发送图片
|
||||||
|
*/
|
||||||
|
clearPendingImages() {
|
||||||
|
for (const img of this.pendingImages) {
|
||||||
|
if (img.blobUrl) {
|
||||||
|
URL.revokeObjectURL(img.blobUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pendingImages = [];
|
||||||
|
this.renderImageThumbnails();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图片预览模态窗
|
||||||
|
* @param {string} imageId - 图片ID
|
||||||
|
*/
|
||||||
|
openImagePreview(imageId) {
|
||||||
|
const img = this.pendingImages.find(i => i.id === imageId);
|
||||||
|
if (!img || !this.el.imagePreviewModal || !this.el.imagePreviewFull) return;
|
||||||
|
|
||||||
|
this.el.imagePreviewFull.src = img.blobUrl;
|
||||||
|
this.el.imagePreviewModal.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭图片预览模态窗
|
||||||
|
*/
|
||||||
|
closeImagePreviewModal() {
|
||||||
|
if (!this.el.imagePreviewModal) return;
|
||||||
|
this.el.imagePreviewModal.classList.remove('active');
|
||||||
|
if (this.el.imagePreviewFull) {
|
||||||
|
this.el.imagePreviewFull.src = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示图片错误提示
|
||||||
|
* @param {string} message - 错误信息
|
||||||
|
*/
|
||||||
|
showImageError(message) {
|
||||||
|
// 创建toast提示
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'image-error-toast';
|
||||||
|
toast.textContent = message;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// 3秒后自动移除
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新图片上传UI状态(根据enableVision配置)
|
||||||
|
*/
|
||||||
|
updateImageUploadUI() {
|
||||||
|
const visionEnabled = this.apiClient.isVisionEnabled();
|
||||||
|
|
||||||
|
// 更新上传按钮状态
|
||||||
|
if (this.el.imageUploadBtn) {
|
||||||
|
this.el.imageUploadBtn.disabled = !visionEnabled;
|
||||||
|
this.el.imageUploadBtn.title = visionEnabled
|
||||||
|
? '上传图片 (支持粘贴)'
|
||||||
|
: '图片解析功能已禁用';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新禁用提示
|
||||||
|
if (this.el.imageVisionDisabledTip) {
|
||||||
|
this.el.imageVisionDisabledTip.classList.toggle('hidden', visionEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果禁用了Vision,清空待发送图片
|
||||||
|
if (!visionEnabled && this.pendingImages.length > 0) {
|
||||||
|
this.clearPendingImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Vision配置UI(处理运行时锁定状态)
|
||||||
|
* @param {Object} detail - 配置详情 { enableVision, isRuntimeLocked }
|
||||||
|
*/
|
||||||
|
updateVisionConfigUI(detail) {
|
||||||
|
if (!this.el.configEnableVision) return;
|
||||||
|
|
||||||
|
const { enableVision, isRuntimeLocked } = detail;
|
||||||
|
|
||||||
|
// 更新复选框状态
|
||||||
|
this.el.configEnableVision.checked = enableVision;
|
||||||
|
|
||||||
|
// 如果被运行时配置锁定,禁用复选框并添加提示
|
||||||
|
if (isRuntimeLocked) {
|
||||||
|
this.el.configEnableVision.disabled = true;
|
||||||
|
|
||||||
|
// 添加锁定提示
|
||||||
|
const parent = this.el.configEnableVision.closest('.flex');
|
||||||
|
if (parent) {
|
||||||
|
let lockHint = parent.querySelector('.vision-lock-hint');
|
||||||
|
if (!lockHint) {
|
||||||
|
lockHint = document.createElement('span');
|
||||||
|
lockHint.className = 'vision-lock-hint text-xs text-orange-600 ml-2';
|
||||||
|
lockHint.innerHTML = '<iconify-icon icon="ph:lock-bold" class="align-middle"></iconify-icon> 由部署配置锁定';
|
||||||
|
parent.appendChild(lockHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.el.configEnableVision.disabled = false;
|
||||||
|
|
||||||
|
// 移除锁定提示
|
||||||
|
const parent = this.el.configEnableVision.closest('.flex');
|
||||||
|
if (parent) {
|
||||||
|
const lockHint = parent.querySelector('.vision-lock-hint');
|
||||||
|
if (lockHint) {
|
||||||
|
lockHint.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将File转换为Base64
|
||||||
|
* @param {File} file - 文件对象
|
||||||
|
* @returns {Promise<string>} - Base64字符串(不含data:前缀)
|
||||||
|
*/
|
||||||
|
fileToBase64(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
// 移除 "data:image/xxx;base64," 前缀
|
||||||
|
const base64 = reader.result.split(',')[1];
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备图片数据用于API发送(并行转换优化)
|
||||||
|
* @returns {Promise<Array>} - 图片数据数组 [{ base64, mimeType }]
|
||||||
|
*/
|
||||||
|
async prepareImagesForSend() {
|
||||||
|
// 并行转换所有图片为base64
|
||||||
|
const conversionPromises = this.pendingImages.map(async (img) => {
|
||||||
|
// 如果还没有转换为base64,现在转换
|
||||||
|
if (!img.base64) {
|
||||||
|
img.base64 = await this.fileToBase64(img.file);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
base64: img.base64,
|
||||||
|
mimeType: img.mimeType
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(conversionPromises);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
global.AppShell = AppShell;
|
global.AppShell = AppShell;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
artifact: {
|
artifact: {
|
||||||
type: 'svg',
|
type: 'svg',
|
||||||
fence: 'svg',
|
fence: 'svg',
|
||||||
startPattern: /```(?:svg)?\s*<svg/i,
|
startPattern: /```\s*(?:svg|xml)?\s*<svg/i,
|
||||||
parser: parseResponse
|
parser: parseResponse
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
artifact: {
|
artifact: {
|
||||||
type: 'svg',
|
type: 'svg',
|
||||||
fence: 'svg',
|
fence: 'svg',
|
||||||
startPattern: /```(?:svg)?\s*<svg/i,
|
startPattern: /```\s*(?:svg|xml)?\s*<svg/i,
|
||||||
parser: parseResponse
|
parser: parseResponse
|
||||||
},
|
},
|
||||||
hooks: {},
|
hooks: {},
|
||||||
|
|||||||
98
js/modules/thinksvg.js
Normal file
98
js/modules/thinksvg.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* 思维导图模块
|
||||||
|
*
|
||||||
|
* 功能:基于用户输入生成 SVG 格式的思维导图
|
||||||
|
* 特性:
|
||||||
|
* - 支持多层级树形结构展示
|
||||||
|
* - 通过提示词引导 AI 生成布局和样式
|
||||||
|
* - 支持 SVG/PNG 导出
|
||||||
|
* - 历史记录持久化
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function registerThinkSvgModule(global) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 确保 ModuleRegistry 已初始化
|
||||||
|
if (!global.ModuleRegistry) {
|
||||||
|
throw new Error('ModuleRegistry 未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 AI 响应,提取 SVG 内容
|
||||||
|
* @param {string} content - AI 返回的原始内容
|
||||||
|
* @returns {Object} 解析结果对象,包含 svgContent、beforeText、afterText 等字段
|
||||||
|
*/
|
||||||
|
const parseResponse = (content) => Utils.parseSVGResponse(content);
|
||||||
|
|
||||||
|
// 注册思维导图模块
|
||||||
|
global.ModuleRegistry.register({
|
||||||
|
// 模块标识
|
||||||
|
id: 'thinksvg',
|
||||||
|
|
||||||
|
// 显示名称
|
||||||
|
label: '思维导图',
|
||||||
|
|
||||||
|
// 图标(使用 Phosphor Icons 的树形结构图标)
|
||||||
|
icon: 'ph:tree-structure-duotone',
|
||||||
|
|
||||||
|
// 渲染器类型
|
||||||
|
renderer: 'svg',
|
||||||
|
|
||||||
|
// 提示词键名(对应 prompts/thinksvg-prompt.txt)
|
||||||
|
promptKey: 'thinksvg',
|
||||||
|
|
||||||
|
// 本地存储命名空间
|
||||||
|
storageNamespace: 'module:thinksvg',
|
||||||
|
|
||||||
|
// 聊天配置
|
||||||
|
chat: {
|
||||||
|
// 输入框占位符
|
||||||
|
placeholder: '输入要梳理的主题或问题,我来生成思维导图…',
|
||||||
|
|
||||||
|
// 流式响应开始标记
|
||||||
|
streamStartToken: '```svg',
|
||||||
|
|
||||||
|
// 上下文窗口大小(保留最近 10 条消息)
|
||||||
|
contextWindow: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
// 产物配置
|
||||||
|
artifact: {
|
||||||
|
// 产物类型
|
||||||
|
type: 'svg',
|
||||||
|
|
||||||
|
// 代码围栏标识
|
||||||
|
fence: 'svg',
|
||||||
|
|
||||||
|
// SVG 开始模式匹配(兼容 ```svg、```xml、``` xml 等格式)
|
||||||
|
startPattern: /```\s*(?:svg|xml)?\s*<svg/i,
|
||||||
|
|
||||||
|
// 内容解析器
|
||||||
|
parser: parseResponse
|
||||||
|
},
|
||||||
|
|
||||||
|
// 生命周期钩子(预留扩展)
|
||||||
|
hooks: {},
|
||||||
|
|
||||||
|
// 导出功能配置
|
||||||
|
exports: {
|
||||||
|
// 允许导出为 SVG 文件
|
||||||
|
allowSvg: true,
|
||||||
|
|
||||||
|
// 允许导出为 PNG 图片
|
||||||
|
allowPng: true,
|
||||||
|
|
||||||
|
// 允许复制到剪贴板
|
||||||
|
allowClipboard: true,
|
||||||
|
|
||||||
|
// 允许查看源代码
|
||||||
|
allowCode: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// UI 配置
|
||||||
|
ui: {
|
||||||
|
// 预览区占位文本
|
||||||
|
placeholderText: '生成的思维导图将在此处显示'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})(window);
|
||||||
@@ -102,7 +102,8 @@
|
|||||||
.filter((msg) => msg.type === 'user' || msg.type === 'ai')
|
.filter((msg) => msg.type === 'user' || msg.type === 'ai')
|
||||||
.map((msg) => ({
|
.map((msg) => ({
|
||||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
// 优先使用原始内容构建上下文,兼容旧数据回退到 content
|
||||||
|
content: msg.rawContent || msg.content || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ function generateId(prefix = 'id') {
|
|||||||
// 解析SVG响应,提取SVG内容和前后文本,容错缺失的结束反引号
|
// 解析SVG响应,提取SVG内容和前后文本,容错缺失的结束反引号
|
||||||
function parseSVGResponse(response = '') {
|
function parseSVGResponse(response = '') {
|
||||||
const content = typeof response === 'string' ? response : String(response || '');
|
const content = typeof response === 'string' ? response : String(response || '');
|
||||||
const svgFenceRegex = /```(?:svg)?\s*([\s\S]*?)```/i;
|
// 兼容 ```svg、```xml、``` xml、``` svg 等格式
|
||||||
|
const svgFenceRegex = /```\s*(?:svg|xml)?\s*([\s\S]*?)```/i;
|
||||||
const fenceMatch = content.match(svgFenceRegex);
|
const fenceMatch = content.match(svgFenceRegex);
|
||||||
|
|
||||||
if (fenceMatch) {
|
if (fenceMatch) {
|
||||||
@@ -40,14 +41,14 @@ function parseSVGResponse(response = '') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兼容缺失结束反引号的情况
|
// 兼容缺失结束反引号的情况(同样兼容 xml 格式)
|
||||||
const svgStartRegex = /```(?:svg)?\s*<svg[\s\S]*$/i;
|
const svgStartRegex = /```\s*(?:svg|xml)?\s*<svg[\s\S]*$/i;
|
||||||
const startMatch = content.match(svgStartRegex);
|
const startMatch = content.match(svgStartRegex);
|
||||||
|
|
||||||
if (startMatch) {
|
if (startMatch) {
|
||||||
const startIndex = startMatch.index;
|
const startIndex = startMatch.index;
|
||||||
const beforeText = content.substring(0, startIndex).trim();
|
const beforeText = content.substring(0, startIndex).trim();
|
||||||
let svgSection = content.substring(startIndex).replace(/```(?:svg)?\s*/i, '').trim();
|
let svgSection = content.substring(startIndex).replace(/```\s*(?:svg|xml)?\s*/i, '').trim();
|
||||||
|
|
||||||
// 去掉尾部残留的反引号
|
// 去掉尾部残留的反引号
|
||||||
svgSection = svgSection.replace(/```$/, '').trim();
|
svgSection = svgSection.replace(/```$/, '').trim();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
我是一名世界级的全栈前端架构师和UI/UX设计师,一个专注于生成高质量网页的AI专家,人称“UPage架构师”。我的工作精确且富有创意,如同顶尖设计机构(如Instrument或Fantasy)的首席工程师。我的核心功能是将用户请求转化为单个、完整、可直接投入生产的HTML文件。
|
我是一名世界级的全栈前端架构师和UI/UX设计师,一个专注于生成高质量网页的AI专家,人称“OnePage架构师”。我的工作精确且富有创意,如同顶尖设计机构(如Instrument或Fantasy)的首席工程师。我的核心功能是将用户请求转化为单个、完整、可直接投入生产的HTML文件。
|
||||||
|
|
||||||
**我的核心使命 (Core Mandate):**
|
**我的核心使命 (Core Mandate):**
|
||||||
我的根本目的是使用纯HTML、Tailwind CSS和原生JavaScript来生成完整的单文件网页。我将最终产品以一个独立的、自包含的HTML文件形式,封装在一个markdown代码块中交付。我绝不生成部分代码、自定义标签或专有格式。
|
我的根本目的是使用纯HTML、Tailwind CSS和原生JavaScript来生成完整的单文件网页。我将最终产品以一个独立的、自包含的HTML文件形式,封装在一个markdown代码块中交付。我绝不生成部分代码、自定义标签或专有格式。
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
**我的操作流程 (Operational Protocol):**
|
**我的操作流程 (Operational Protocol):**
|
||||||
1. **分析与规划 (Analyze & Plan):** 我首先会分析用户的需求,并制定一个简明的实现计划。我将概述我将采取的具体步骤、关键的设计元素,并简要提及潜在的挑战。这个计划将以自然语言描述,简洁(2-4行)。
|
1. **分析与规划 (Analyze & Plan):** 我首先会分析用户的需求,并制定一个简明的实现计划。我将概述我将采取的具体步骤、关键的设计元素,并简要提及潜在的挑战。这个计划将以自然语言描述,简洁(2-4行)。
|
||||||
2. **生成代码 (Generate Code):** 我将根据规划,编写完整的、统一的HTML文件。这个文件包括HTML结构、用于Tailwind CSS定制的`<style>`标签,以及任何必需的JavaScript代码。
|
2. **生成代码 (Generate Code):** 我将根据规划,编写完整的、统一的HTML文件。这个文件包括HTML结构、用于Tailwind CSS定制的`<style>`标签,以及任何必需的JavaScript代码。
|
||||||
3. **交付与总结 (Deliver & Summarize):** 我将最终的HTML文件呈现在一个markdown代码块中。代码之后,我将提供一份简短的工作总结。
|
3. **交付与总结 (Deliver & Summarize):** 我将最终的HTML文件呈现在一个markdown代码块中。代码之后,我将提供一份简短的工作总结。我要保证每次对话只输出唯一markdown代码块,并保证是完整的HTML
|
||||||
|
|
||||||
**我的技术栈与限制 (Tech Stack & Constraints):**
|
**我的技术栈与限制 (Tech Stack & Constraints):**
|
||||||
* **技术 (Technology):** 我仅使用原生HTML、CSS和JavaScript进行构建。我不会使用任何前端框架,如React、Vue或Angular。
|
* **技术 (Technology):** 我仅使用原生HTML、CSS和JavaScript进行构建。我不会使用任何前端框架,如React、Vue或Angular。
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
* **丰富交互 (Rich Interactivity):** 我为交互而设计,实现精致的悬停效果、流畅的动画过渡、视差滚动和滚动触发事件,使页面充满活力。
|
* **丰富交互 (Rich Interactivity):** 我为交互而设计,实现精致的悬停效果、流畅的动画过渡、视差滚动和滚动触发事件,使页面充满活力。
|
||||||
* **结构深度与复杂性 (Structural Depth & Complexity):** 我的区块(section)具有多层次内容,包括主要信息、支持数据和装饰元素,以创建视觉和信息层次。我确保每个区块包含至少6个精心设计的子元素,构建多层次结构。
|
* **结构深度与复杂性 (Structural Depth & Complexity):** 我的区块(section)具有多层次内容,包括主要信息、支持数据和装饰元素,以创建视觉和信息层次。我确保每个区块包含至少6个精心设计的子元素,构建多层次结构。
|
||||||
* **内容多样性 (Content Diversity):** 我采用多种内容呈现方式,如网格、卡片、时间轴和图文混排布局,以避免单调。
|
* **内容多样性 (Content Diversity):** 我采用多种内容呈现方式,如网格、卡片、时间轴和图文混排布局,以避免单调。
|
||||||
* **品牌与色彩一致性 (Brand & Color Consistency):** 我在整个页面中建立并严格遵循明确的配色方案(主色、辅色、强调色),以实现统一的品牌体验。所有元素,从按钮到边框,都遵循此配色方案。
|
* **品牌与色彩一致性 (Brand & Color Consistency):** 我在整个页面中建立并严格遵循明确的配色方案(主色、辅色、强调色),以实现统一的品牌体验。我会注意前景色和背景色,不会让文字和背景过于重叠导致文字看不清。所有元素,从按钮到边框,都遵循此配色方案。
|
||||||
* **高页面密度 (High Page Density):** 我会生成足够数量的区块以构成一个完整而全面的页面,并根据其目的进行定制(例如,企业页面至少8个区块,仪表盘至少6个区块)。简单的页面(如联系表单)是例外,但其功能将是完整的。
|
* **高页面密度 (High Page Density):** 我会生成足够数量的区块以构成一个完整而全面的页面,并根据其目的进行定制(例如,企业页面至少8个区块,仪表盘至少6个区块)。简单的页面(如联系表单)是例外,但其功能将是完整的。
|
||||||
|
|
||||||
**我的边界条件 (Boundary Conditions):**
|
**我的边界条件 (Boundary Conditions):**
|
||||||
我专注于网页构建任务。对于超出此范围的任何请求,我将说明我的专业职责并拒绝请求,回复如下:“十分抱歉,我是由凌霞软件开发的网页构建工具 UPage,专注与网页构建,因此我无法回答与网页构建无关的问题。”
|
我专注于网页构建任务。对于超出此范围的任何请求,我将说明我的专业职责并拒绝请求,回复如下:“十分抱歉 OnePage,专注与网页构建,因此我无法回答与网页构建无关的问题。”
|
||||||
|
|
||||||
**行为示例 (Behavior Examples):**
|
**行为示例 (Behavior Examples):**
|
||||||
|
|
||||||
|
|||||||
79
prompts/thinksvg-prompt.txt
Normal file
79
prompts/thinksvg-prompt.txt
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
请根据用户的需求生成一个详细的思维导图分析,并以SVG格式返回,注意使用markdown格式回复。
|
||||||
|
|
||||||
|
|
||||||
|
下面是SVG模板,注意模板中定义了三级,你可以参考布局和样式,你可以根据情况自定义层级
|
||||||
|
```
|
||||||
|
<svg width="720" height="280" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- 背景 -->
|
||||||
|
<rect width="100%" height="100%" fill="#ffffff" />
|
||||||
|
<style>
|
||||||
|
.node-text { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 13px; fill: #555; }
|
||||||
|
.node-title { font-weight: bold; font-size: 14px; fill: #333; }
|
||||||
|
.l2-text { font-weight: bold; font-size: 13px; }
|
||||||
|
.number-circle { font-size: 10px; font-weight: bold; fill: #fff; }
|
||||||
|
.branch-path { fill: none; stroke-width: 1.5; opacity: 0.4; }
|
||||||
|
</style>
|
||||||
|
<g transform="translate(20, 25)">
|
||||||
|
<!-- 连线部分 (中心点 Y=115) -->
|
||||||
|
<g class="branch-path">
|
||||||
|
<path d="M140,115 C180,115 180,22 210,22" stroke="#e74c3c" />
|
||||||
|
<path d="M140,115 C180,115 180,67 210,67" stroke="#f39c12" />
|
||||||
|
<path d="M140,115 C180,115 180,112 210,112" stroke="#f1c40f" />
|
||||||
|
<path d="M140,115 C180,115 180,157 210,157" stroke="#2ecc71" />
|
||||||
|
<path d="M140,115 C180,115 180,202 210,202" stroke="#3498db" />
|
||||||
|
<!-- L2 to L3 短直线 -->
|
||||||
|
<path d="M340,22 L360,22" stroke="#e74c3c" />
|
||||||
|
<path d="M340,67 L360,67" stroke="#f39c12" />
|
||||||
|
<path d="M340,112 L360,112" stroke="#f1c40f" />
|
||||||
|
<path d="M340,157 L360,157" stroke="#2ecc71" />
|
||||||
|
<path d="M340,202 L360,202" stroke="#3498db" />
|
||||||
|
</g>
|
||||||
|
<!-- 一级中心节点 -->
|
||||||
|
<g transform="translate(0, 85)">
|
||||||
|
<rect x="0" y="0" width="140" height="60" rx="6" fill="#fff" stroke="#333" stroke-width="2" />
|
||||||
|
<circle cx="22" cy="30" r="8" fill="#ff4757" />
|
||||||
|
<path d="M20 25 L26 30 L20 35 Z" fill="white" />
|
||||||
|
<text x="38" y="27" class="node-title">软件架构风格</text>
|
||||||
|
<text x="38" y="45" class="node-title">必会考点</text>
|
||||||
|
</g>
|
||||||
|
<!-- 二级节点 (线框) 与 三级节点 (纯文字) -->
|
||||||
|
<!-- 1. 间距从75压缩至45 -->
|
||||||
|
<g transform="translate(210, 5)">
|
||||||
|
<rect x="0" y="0" width="130" height="34" rx="17" fill="none" stroke="#e74c3c" stroke-width="1.5" />
|
||||||
|
<circle cx="17" cy="17" r="9" fill="#e74c3c" />
|
||||||
|
<text x="17" y="21" class="number-circle" text-anchor="middle">1</text>
|
||||||
|
<text x="32" y="22" class="l2-text" fill="#e74c3c">数据流风格</text>
|
||||||
|
<text x="160" y="22" class="node-text">包括 <tspan font-weight="bold">批处理序列</tspan> 和 <tspan font-weight="bold">管道/过滤器</tspan></text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(210, 50)">
|
||||||
|
<rect x="0" y="0" width="130" height="34" rx="17" fill="none" stroke="#f39c12" stroke-width="1.5" />
|
||||||
|
<circle cx="17" cy="17" r="9" fill="#f39c12" />
|
||||||
|
<text x="17" y="21" class="number-circle" text-anchor="middle">2</text>
|
||||||
|
<text x="32" y="22" class="l2-text" fill="#f39c12">调用/返回风格</text>
|
||||||
|
<text x="160" y="22" class="node-text">包括 <tspan font-weight="bold">主程序/子程序</tspan>、<tspan font-weight="bold">面向对象</tspan> 及 <tspan font-weight="bold">层次结构</tspan></text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(210, 95)">
|
||||||
|
<rect x="0" y="0" width="130" height="34" rx="17" fill="none" stroke="#f1c40f" stroke-width="1.5" />
|
||||||
|
<circle cx="17" cy="17" r="9" fill="#f1c40f" />
|
||||||
|
<text x="17" y="21" class="number-circle" text-anchor="middle">3</text>
|
||||||
|
<text x="32" y="22" class="l2-text" fill="#f1c40f">独立构件风格</text>
|
||||||
|
<text x="160" y="22" class="node-text">包括 <tspan font-weight="bold">进程通信</tspan> 和 <tspan font-weight="bold">事件驱动</tspan> 系统</text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(210, 140)">
|
||||||
|
<rect x="0" y="0" width="130" height="34" rx="17" fill="none" stroke="#2ecc71" stroke-width="1.5" />
|
||||||
|
<circle cx="17" cy="17" r="9" fill="#2ecc71" />
|
||||||
|
<text x="17" y="21" class="number-circle" text-anchor="middle">4</text>
|
||||||
|
<text x="32" y="22" class="l2-text" fill="#2ecc71">虚拟机风格</text>
|
||||||
|
<text x="160" y="22" class="node-text">包括 <tspan font-weight="bold">解释器</tspan> 和 <tspan font-weight="bold">基于规则的系统</tspan></text>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(210, 185)">
|
||||||
|
<rect x="0" y="0" width="130" height="34" rx="17" fill="none" stroke="#3498db" stroke-width="1.5" />
|
||||||
|
<circle cx="17" cy="17" r="9" fill="#3498db" />
|
||||||
|
<text x="17" y="21" class="number-circle" text-anchor="middle">5</text>
|
||||||
|
<text x="32" y="22" class="l2-text" fill="#3498db">仓库风格</text>
|
||||||
|
<text x="160" y="22" class="node-text">包括 <tspan font-weight="bold">数据库</tspan>、<tspan font-weight="bold">黑板</tspan> 和 <tspan font-weight="bold">超文本</tspan></text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
```
|
||||||
BIN
设计/images/onepage.png
Normal file
BIN
设计/images/onepage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
197
设计/原型.html
197
设计/原型.html
@@ -14,61 +14,63 @@
|
|||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 狂野线条效果 */
|
/* Apple 风格边框 */
|
||||||
.wild-border {
|
.apple-card {
|
||||||
border: 3px solid;
|
border: 1px solid rgba(0,0,0,0.08);
|
||||||
box-shadow: 4px 4px 0px rgba(0,0,0,0.3);
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.07), 0 1px 3px rgba(0,0,0,0.06);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 切换按钮激活状态 */
|
/* 切换按钮激活状态 - Apple 风格 */
|
||||||
.mode-btn-active {
|
.mode-btn-active {
|
||||||
transform: translateY(-2px);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
box-shadow: 0 4px 0 rgba(0,0,0,0.3);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-btn-inactive {
|
.mode-btn-inactive {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对话气泡样式 */
|
/* 对话气泡样式 - Google 风格 */
|
||||||
.chat-bubble-user {
|
.chat-bubble-user {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: #4C76AB;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px 14px;
|
padding: 12px 16px;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
border: 2px solid #000;
|
border-radius: 18px 18px 4px 18px;
|
||||||
box-shadow: 2px 2px 0 rgba(0,0,0,0.2);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai {
|
.chat-bubble-ai {
|
||||||
background: #fff;
|
background: #f8f9fa;
|
||||||
color: #1f2937;
|
color: #464646;
|
||||||
padding: 10px 14px;
|
padding: 12px 16px;
|
||||||
max-width: 85%;
|
max-width: 85%;
|
||||||
border: 2px solid #10b981;
|
border-radius: 4px 18px 18px 18px;
|
||||||
box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3);
|
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SVG占位符样式 - 块级换行 + 新配色 */
|
/* SVG占位符样式 - Google 风格 */
|
||||||
.svg-placeholder-block {
|
.svg-placeholder-block {
|
||||||
display: block;
|
display: block;
|
||||||
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
|
background: #2B4269;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 8px 14px;
|
padding: 10px 16px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
border: 2px solid #000;
|
border-radius: 8px;
|
||||||
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-placeholder-block:hover {
|
.svg-placeholder-block:hover {
|
||||||
transform: translateX(2px) translateY(-2px);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.16), 0 2px 4px rgba(0,0,0,0.08);
|
||||||
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
transform: translateY(-1px);
|
||||||
background: linear-gradient(135deg, #fb923c 0%, #f87171 100%);
|
background: #5A6270;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 气泡操作按钮 */
|
/* 气泡操作按钮 */
|
||||||
@@ -113,28 +115,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background: white;
|
background: rgba(255,255,255,0.95);
|
||||||
border: 4px solid #000;
|
border-radius: 20px;
|
||||||
box-shadow: 8px 8px 0 rgba(0,0,0,0.4);
|
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1),
|
||||||
|
0 10px 10px -5px rgba(0,0,0,0.04);
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表单输入框样式 */
|
/* 表单输入框样式 - Google 风格 */
|
||||||
.config-input {
|
.config-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 12px 16px;
|
||||||
border: 2px solid #000;
|
border: 1px solid #dadce0;
|
||||||
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
color: #464646;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-input:focus {
|
.config-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #667eea;
|
border-color: #4C76AB;
|
||||||
box-shadow: 3px 3px 0 rgba(102, 126, 234, 0.3);
|
box-shadow: 0 0 0 2px rgba(76, 118, 171, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 齿轮旋转动画 */
|
/* 齿轮旋转动画 */
|
||||||
@@ -148,31 +155,31 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100 h-screen flex flex-col">
|
<body class="bg-gray-50 h-screen flex flex-col" style="background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);">
|
||||||
|
|
||||||
<!-- 顶部标题栏 -->
|
<!-- 顶部标题栏 -->
|
||||||
<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="px-6 py-4 flex items-center justify-between" style="background: rgba(255,255,255,0.8); backdrop-filter: blur(20px); border-bottom: 1px solid rgba(0,0,0,0.06);">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-4">
|
||||||
<iconify-icon icon="ph:lightning-fill" class="text-3xl text-white"></iconify-icon>
|
<iconify-icon icon="ph:lightning-fill" class="text-3xl" style="color: #4C76AB;"></iconify-icon>
|
||||||
<h1 id="page-title" class="text-2xl font-black text-white tracking-tight">产品画布</h1>
|
<h1 id="page-title" class="text-2xl font-semibold tracking-tight" style="color: #464646;">产品画布</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧按钮组 -->
|
<!-- 右侧按钮组 -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-4">
|
||||||
<!-- 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 p-3 rounded-full hover:bg-gray-100 transition-all" style="color: #666666;" title="API配置">
|
||||||
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
|
<iconify-icon icon="ph:gear-six-fill" class="text-xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="text-white font-bold text-sm">点击切换模式</span>
|
<span class="font-medium text-sm" style="color: #888888;">点击切换模式</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 wave-hand" style="color: #5A6270;"></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 px-5 py-2.5 font-medium rounded-full hover:bg-gray-100 transition-all duration-300" style="color: #464646; background: rgba(76,118,171,0.1);">
|
||||||
<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-2"></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 px-5 py-2.5 font-medium rounded-full hover:bg-gray-100 transition-all duration-300" style="color: #666666;">
|
||||||
<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-2"></iconify-icon>
|
||||||
SWOT分析
|
SWOT分析
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +189,7 @@
|
|||||||
<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 apple-card flex flex-col">
|
||||||
<!-- 对话历史区 -->
|
<!-- 对话历史区 -->
|
||||||
<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">
|
||||||
|
|
||||||
@@ -253,40 +260,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 输入区 -->
|
<!-- 输入区 -->
|
||||||
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
|
<div class="p-5 border-t bg-white" style="border-color: rgba(0,0,0,0.06);">
|
||||||
<div class="relative flex items-center gap-2">
|
<div class="relative flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
id="chat-input"
|
id="chat-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="输入您的想法,按Enter发送..."
|
placeholder="输入您的想法,按Enter发送..."
|
||||||
class="flex-1 p-2 border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
|
class="flex-1 px-5 py-4 border focus:outline-none transition-all font-normal rounded-2xl"
|
||||||
|
style="border-color: rgba(0,0,0,0.08); color: #464646; background: rgba(248,249,250,0.8);"
|
||||||
|
onfocus="this.style.borderColor='#4C76AB'; this.style.boxShadow='0 0 0 3px rgba(76,118,171,0.15)'; this.style.background='rgba(255,255,255,0.95)'"
|
||||||
|
onblur="this.style.borderColor='rgba(0,0,0,0.08)'; this.style.boxShadow='none'; this.style.background='rgba(248,249,250,0.8)'"
|
||||||
/>
|
/>
|
||||||
<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="p-3 rounded-full transition-all hover:scale-105" style="color: white; background: #4C76AB; box-shadow: 0 2px 8px rgba(76,118,171,0.3);" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
||||||
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
|
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-xl"></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 apple-card 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-8 overflow-auto" style="background: linear-gradient(135deg, #fafafa 0%, #f8f9fa 100%);">
|
||||||
<div id="svg-placeholder" class="text-center text-gray-400">
|
<div id="svg-placeholder" class="text-center" style="color: #787878;">
|
||||||
<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" style="color: #5586F5;"></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-5 border-t flex justify-end items-center gap-4" style="border-color: rgba(0,0,0,0.06); background: rgba(248,249,250,0.8);">
|
||||||
<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="download-svg-btn" class="p-3 text-white rounded-xl transition-all" style="background: #5A6270; box-shadow: 0 4px 12px rgba(90,98,112,0.25);" title="下载SVG" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
|
||||||
<iconify-icon icon="mdi:download-outline" class="text-xl"></iconify-icon>
|
<iconify-icon icon="mdi:download-outline" class="text-lg"></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="export-image-btn" class="p-3 text-white rounded-xl transition-all" style="background: #2B4269; box-shadow: 0 4px 12px rgba(43,66,105,0.25);" title="导出为图片" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
|
||||||
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
|
<iconify-icon icon="mdi:image-outline" class="text-lg"></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="view-code-btn" class="p-3 text-white rounded-xl transition-all" style="background: #4C76AB; box-shadow: 0 4px 12px rgba(76,118,171,0.25);" title="查看代码" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
|
||||||
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
|
<iconify-icon icon="mdi:code-tags" class="text-lg"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,13 +307,13 @@
|
|||||||
<div id="config-modal" class="modal-overlay">
|
<div id="config-modal" class="modal-overlay">
|
||||||
<div class="modal-content">
|
<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="p-6 border-b flex items-center justify-between" style="background: rgba(255,255,255,0.95); border-color: rgba(0,0,0,0.06); border-radius: 20px 20px 0 0;">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<iconify-icon icon="ph:plugs-connected-fill" class="text-3xl text-white"></iconify-icon>
|
<iconify-icon icon="ph:plugs-connected-fill" class="text-2xl" style="color: #4C76AB;"></iconify-icon>
|
||||||
<h2 class="text-xl font-black text-white">API 配置</h2>
|
<h2 class="text-xl font-semibold" style="color: #464646;">API 配置</h2>
|
||||||
</div>
|
</div>
|
||||||
<button id="close-modal-btn" class="text-white hover:bg-white/20 p-2 transition-all">
|
<button id="close-modal-btn" class="hover:bg-gray-100 p-3 rounded-full transition-all" style="color: #666666;" onmouseover="this.style.backgroundColor='#f0f0f0'" onmouseout="this.style.backgroundColor='transparent'">
|
||||||
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
|
<iconify-icon icon="ph:x-bold" class="text-xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -311,8 +321,8 @@
|
|||||||
<div class="p-6 space-y-4">
|
<div class="p-6 space-y-4">
|
||||||
<!-- API URL -->
|
<!-- API URL -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<label class="block font-semibold mb-3 flex items-center gap-3" style="color: #464646;">
|
||||||
<iconify-icon icon="ph:link-bold" class="text-lg text-blue-600"></iconify-icon>
|
<iconify-icon icon="ph:link-bold" class="text-lg" style="color: #4C76AB;"></iconify-icon>
|
||||||
API URL
|
API URL
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -326,8 +336,8 @@
|
|||||||
|
|
||||||
<!-- API Key -->
|
<!-- API Key -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<label class="block font-semibold mb-3 flex items-center gap-3" style="color: #464646;">
|
||||||
<iconify-icon icon="ph:key-bold" class="text-lg text-green-600"></iconify-icon>
|
<iconify-icon icon="ph:key-bold" class="text-lg" style="color: #2B4269;"></iconify-icon>
|
||||||
API Key
|
API Key
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -341,8 +351,8 @@
|
|||||||
|
|
||||||
<!-- Model -->
|
<!-- Model -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
<label class="block font-semibold mb-3 flex items-center gap-3" style="color: #464646;">
|
||||||
<iconify-icon icon="ph:robot-bold" class="text-lg text-purple-600"></iconify-icon>
|
<iconify-icon icon="ph:robot-bold" class="text-lg" style="color: #5A6270;"></iconify-icon>
|
||||||
模型 (Model)
|
模型 (Model)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -355,19 +365,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 状态显示 -->
|
<!-- 状态显示 -->
|
||||||
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
|
<div id="config-status" class="p-4 border rounded-2xl bg-gray-50 text-sm hidden" style="border-color: rgba(0,0,0,0.06); color: #888888;">
|
||||||
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||||
<span id="status-text">等待操作...</span>
|
<span id="status-text">等待操作...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 模态窗底部按钮 -->
|
<!-- 模态窗底部按钮 -->
|
||||||
<div class="p-4 border-t-3 border-gray-300 bg-gray-100 flex gap-3 justify-end">
|
<div class="p-6 border-t bg-white flex gap-4 justify-end" style="border-color: rgba(0,0,0,0.06); border-radius: 0 0 20px 20px;">
|
||||||
<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">
|
<button id="test-api-btn" class="px-6 py-3 text-white font-medium rounded-2xl transition-all flex items-center gap-3" style="background: #5A6270; box-shadow: 0 4px 12px rgba(90,98,112,0.25);" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
|
||||||
<iconify-icon icon="ph:flask-bold"></iconify-icon>
|
<iconify-icon icon="ph:flask-bold"></iconify-icon>
|
||||||
测试连接
|
测试连接
|
||||||
</button>
|
</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">
|
<button id="save-config-btn" class="px-6 py-3 text-white font-medium rounded-2xl transition-all flex items-center gap-3" style="background: #4C76AB; box-shadow: 0 4px 12px rgba(76,118,171,0.25);" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
|
||||||
<iconify-icon icon="ph:floppy-disk-bold"></iconify-icon>
|
<iconify-icon icon="ph:floppy-disk-bold"></iconify-icon>
|
||||||
保存配置
|
保存配置
|
||||||
</button>
|
</button>
|
||||||
@@ -505,19 +515,22 @@
|
|||||||
configStatus.classList.remove('hidden');
|
configStatus.classList.remove('hidden');
|
||||||
statusText.textContent = message;
|
statusText.textContent = message;
|
||||||
|
|
||||||
configStatus.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
configStatus.style.borderColor = '#787878';
|
||||||
configStatus.classList.remove('border-green-500', 'bg-green-50', 'text-green-700');
|
configStatus.style.backgroundColor = '#f9fafb';
|
||||||
configStatus.classList.remove('border-red-500', 'bg-red-50', 'text-red-700');
|
configStatus.style.color = '#787878';
|
||||||
configStatus.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
|
||||||
|
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
configStatus.classList.add('border-green-500', 'bg-green-50', 'text-green-700');
|
configStatus.style.borderColor = '#47A74F';
|
||||||
|
configStatus.style.backgroundColor = '#e8f5e9';
|
||||||
|
configStatus.style.color = '#2e7d32';
|
||||||
} else if (type === 'error') {
|
} else if (type === 'error') {
|
||||||
configStatus.classList.add('border-red-500', 'bg-red-50', 'text-red-700');
|
configStatus.style.borderColor = '#E04639';
|
||||||
|
configStatus.style.backgroundColor = '#ffebee';
|
||||||
|
configStatus.style.color = '#c62828';
|
||||||
} else if (type === 'loading') {
|
} else if (type === 'loading') {
|
||||||
configStatus.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
configStatus.style.borderColor = '#5586F5';
|
||||||
} else {
|
configStatus.style.backgroundColor = '#e3f2fd';
|
||||||
configStatus.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
configStatus.style.color = '#1565c0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user