Compare commits

..

5 Commits

Author SHA1 Message Date
史悦
b0c487a4ef 6. 增加一个svg放大缩小功能;
7. 目前的svg下载,下载图片,显示代码实现有问题,好像没找到svg;再增加一个复制图片到剪切板功能;
8. 点重新生成按钮,应该再添加一个气泡啊,而且流式响应,现在点击重新生成,就时等待,没有实时显示;
9. 点击查看画布,没有区分现在显示的是哪个气泡或占位符,需要标记区分下;
2025-10-24 20:31:49 +08:00
史悦
cbf59e3450 - js/app.js:165-260, 310-420, 560-893:重构模式切换与流式渲染逻辑;引入 renderSvgViewerForMode 分离各
模式历史与SVG;通过 buildActionToolbar 和 renderConversationHistory 仅为最新 AI 气泡保留“重新生成”;
    新增 setSendButtonState、startStreamingMessage、cancelActiveStream 实现终止按钮与流式中断,回滚后同
    步复位查看面板。
  - js/utils.js:24-334:增强 parseSVGResponse 容错截断SVG/反引号;StreamProcessor 增加完成态管理;
    createStreamRequest 返回 {cancel, finished} 并支持 AbortController,保证中止时回调依然收尾。
  - js/apiclient.js:150-208:流式接口返回新的句柄对象而非简单等待,使前端可触发中止,同时保持异常转换。
  - css/style.css:220-224:去除悬浮显隐,操作按钮常显,并保留色彩过渡以提示交互。
  - js/app.js:900-970:regenerateMessage 改用“处理中”状态并重绘气泡(按最新可见状态),避免历史重复
    生成。
2025-10-24 19:56:40 +08:00
史悦
06e1d5ca19 修改了提示词 2025-10-24 19:18:03 +08:00
史悦
64e93d25b8 - 在 js/utils.js:24-79 强化 parseSVGResponse,兼容缺失结尾反引号和截断的 SVG,自动补齐 </svg> 并去除残
留的 ```,确保后续渲染能稳定提取图形。
2025-10-24 19:03:40 +08:00
史悦
bacafd66dc 修复气泡 2025-10-24 18:52:18 +08:00
7 changed files with 2653 additions and 2129 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -219,12 +219,18 @@ body {
/* 气泡操作按钮 */
.bubble-action-btn {
opacity: 0;
transition: opacity 0.2s;
opacity: 1;
transition: color 0.2s ease;
}
.chat-bubble-ai:hover .bubble-action-btn {
opacity: 1;
.svg-placeholder-active {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
}
.svg-content-wrapper {
display: inline-block;
transform-origin: center top;
}
/* 小手摇摆动画 */
@@ -349,3 +355,7 @@ body {
.clear-history-btn:hover iconify-icon {
animation: shake 0.5s ease-in-out;
}
#send-button.terminate-mode {
border-color: #dc2626;
}

View File

@@ -95,9 +95,21 @@
<!-- 底部操作栏 -->
<div class="p-3 border-t-3 border-gray-300 flex justify-end items-center gap-2 bg-gray-800">
<button id="zoom-out-btn" class="p-2 bg-white text-gray-700 border-2 border-black hover:bg-gray-200 transition-all" title="缩小">
<iconify-icon icon="ph:magnifying-glass-minus-bold" class="text-xl"></iconify-icon>
</button>
<button id="zoom-in-btn" class="p-2 bg-white text-gray-700 border-2 border-black hover:bg-gray-200 transition-all" title="放大">
<iconify-icon icon="ph:magnifying-glass-plus-bold" class="text-xl"></iconify-icon>
</button>
<button id="zoom-reset-btn" class="p-2 bg-white text-gray-700 border-2 border-black hover:bg-gray-200 transition-all" title="重置缩放">
<iconify-icon icon="ph:arrow-counter-clockwise-bold" class="text-xl"></iconify-icon>
</button>
<button id="download-svg-btn" class="p-2 bg-orange-500 text-white border-2 border-black hover:bg-orange-600 transition-all" title="下载SVG">
<iconify-icon icon="mdi:download-outline" class="text-xl"></iconify-icon>
</button>
<button id="copy-image-btn" class="p-2 bg-yellow-500 text-white border-2 border-black hover:bg-yellow-600 transition-all" title="复制图片到剪贴板">
<iconify-icon icon="ph:clipboard-bold" class="text-xl"></iconify-icon>
</button>
<button id="export-image-btn" class="p-2 bg-green-500 text-white border-2 border-black hover:bg-green-600 transition-all" title="导出为图片">
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
</button>

View File

@@ -133,7 +133,7 @@ class APIClient {
{ role: 'user', content: userRequest }
];
return await this.sendChatMessage(messages, { maxTokens: 3000 });
return await this.sendChatMessage(messages, { maxTokens: 18000 });
}
// 生成SWOT分析的专用方法
@@ -144,7 +144,7 @@ class APIClient {
{ role: 'user', content: userRequest }
];
return await this.sendChatMessage(messages, { maxTokens: 3000 });
return await this.sendChatMessage(messages, { maxTokens: 18000 });
}
// 流式生成产品画布
@@ -155,7 +155,7 @@ class APIClient {
{ role: 'user', content: userRequest }
];
return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete);
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
}
// 流式生成SWOT分析
@@ -166,7 +166,7 @@ class APIClient {
{ role: 'user', content: userRequest }
];
return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete);
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
}
// 流式发送聊天请求
@@ -190,7 +190,7 @@ class APIClient {
const url = this.config.url.replace('/chat/completions', '/chat/completions');
try {
await Utils.createStreamRequest(
return Utils.createStreamRequest(
url,
{
method: 'POST',

1002
js/app.js

File diff suppressed because it is too large Load Diff

View File

@@ -21,18 +21,52 @@ function generateId(prefix = 'id') {
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// 解析SVG响应提取SVG内容和前后文本
function parseSVGResponse(response) {
const svgRegex = /```svg\s*([\s\S]*?)```/i;
const match = response.match(svgRegex);
// 解析SVG响应提取SVG内容和前后文本,容错缺失的结束反引号
function parseSVGResponse(response = '') {
const content = typeof response === 'string' ? response : String(response || '');
const svgFenceRegex = /```(?:svg)?\s*([\s\S]*?)```/i;
const fenceMatch = content.match(svgFenceRegex);
if (match) {
const svgContent = match[1].trim();
const beforeText = response.substring(0, match.index).trim();
const afterText = response.substring(match.index + match[0].length).trim();
if (fenceMatch) {
const svgBody = fenceMatch[1].trim();
const beforeText = content.substring(0, fenceMatch.index).trim();
let afterText = content.substring(fenceMatch.index + fenceMatch[0].length).trim();
afterText = afterText.replace(/^\s*```/, '').trim();
return {
svgContent,
svgContent: svgBody,
beforeText,
afterText
};
}
// 兼容缺失结束反引号的情况
const svgStartRegex = /```(?:svg)?\s*<svg[\s\S]*$/i;
const startMatch = content.match(svgStartRegex);
if (startMatch) {
const startIndex = startMatch.index;
const beforeText = content.substring(0, startIndex).trim();
let svgSection = content.substring(startIndex).replace(/```(?:svg)?\s*/i, '').trim();
// 去掉尾部残留的反引号
svgSection = svgSection.replace(/```$/, '').trim();
// 拆分 SVG 正文与额外文本
let afterText = '';
const svgEndIndex = svgSection.lastIndexOf('</svg>');
if (svgEndIndex !== -1) {
afterText = svgSection.substring(svgEndIndex + 6).replace(/```/, '').trim();
svgSection = svgSection.substring(0, svgEndIndex + 6).trim();
}
// 补齐缺失的结束标签
if (svgSection && !svgSection.endsWith('</svg>')) {
svgSection += '\n</svg>';
}
return {
svgContent: svgSection || null,
beforeText,
afterText
};
@@ -40,7 +74,7 @@ function parseSVGResponse(response) {
return {
svgContent: null,
beforeText: response,
beforeText: content.trim(),
afterText: ''
};
}
@@ -204,6 +238,15 @@ class StreamProcessor {
this.onChunk = onChunk;
this.onComplete = onComplete;
this.buffer = '';
this.completed = false;
}
complete(info = {}) {
if (this.completed) return;
this.completed = true;
if (typeof this.onComplete === 'function') {
this.onComplete(info);
}
}
// 处理数据块
@@ -221,7 +264,7 @@ class StreamProcessor {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
this.onComplete();
this.complete({ aborted: false });
return;
}
@@ -237,12 +280,15 @@ class StreamProcessor {
}
// 创建流式请求
async function createStreamRequest(url, options, onChunk, onComplete) {
function createStreamRequest(url, options, onChunk, onComplete) {
const processor = new StreamProcessor(onChunk, onComplete);
const controller = new AbortController();
const fetchPromise = (async () => {
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...options.headers,
'Accept': 'text/event-stream',
@@ -263,10 +309,27 @@ async function createStreamRequest(url, options, onChunk, onComplete) {
const chunk = decoder.decode(value, { stream: true });
processor.processChunk(chunk);
if (processor.completed) {
break;
}
}
if (!processor.completed) {
processor.complete({ aborted: false });
}
} catch (error) {
if (error.name === 'AbortError') {
processor.complete({ aborted: true });
return;
}
throw error;
}
})();
return {
cancel: () => controller.abort(),
finished: fetchPromise
};
}
// 导出工具函数

View File

@@ -3,7 +3,11 @@
请用中文回复并在回复中包含SVG格式的产品画布图表。
产品精益画布助手下面是SVG画布的模板注意使用markdown格式回复
产品精益画布助手下面是SVG画布的模板注意使用markdown格式回复
- 解决方案、门槛优势、关键指标、渠道 文字不要超过7行
- 成本分析、收入分析 文字不要超过6行
```
<svg width="900" height="550" viewBox="0 0 900 550" xmlns="http://www.w3.org/2000/svg" font-family="'PingFang SC', 'Microsoft YaHei', sans-serif">
<defs>
@@ -71,12 +75,6 @@
<g transform="translate(75, 20)">
<text class="title" fill="#f57c00">独特卖点</text>
</g>
<g transform="translate(75, 60)">
<text class="desc" style="font-size: 14px; font-weight: bold;" fill="#f57c00">
<tspan x="0" dy="0">微信扫一扫,</tspan>
<tspan x="0" dy="20">老少皆宜智能回收</tspan>
</text>
</g>
<text x="10" y="120" class="content-bold">对用户价值:</text>
<text x="10" y="132" class="content">• 扫码即用,操作超简单</text>
<text x="10" y="144" class="content">• 价格透明,立即到账</text>