Compare commits
2 Commits
b0c487a4ef
...
c83c36baa6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c83c36baa6 | ||
|
|
4b14bb26dd |
@@ -2,6 +2,15 @@ body {
|
|||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 统一处理 Iconify 图标的对齐方式,避免在按钮与文字中出现垂直偏移 */
|
||||||
|
iconify-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* 狂野线条效果 */
|
/* 狂野线条效果 */
|
||||||
.wild-border {
|
.wild-border {
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
@@ -223,9 +232,55 @@ body {
|
|||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 消息删除浮动按钮 */
|
||||||
|
.message-with-delete {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 2px solid #1f2937;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: #dc2626;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 2px 2px 0 rgba(0,0,0,0.2);
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-delete-btn:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #ffffff;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-delete-btn iconify-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-placeholder-block {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes svg-active-pulse {
|
||||||
|
0% { box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35), 0 6px 12px rgba(15, 23, 42, 0.15); }
|
||||||
|
50% { box-shadow: 0 0 0 6px rgba(37, 99, 235, 0.15), 0 10px 18px rgba(15, 23, 42, 0.2); }
|
||||||
|
100% { box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35), 0 6px 12px rgba(15, 23, 42, 0.15); }
|
||||||
|
}
|
||||||
|
|
||||||
.svg-placeholder-active {
|
.svg-placeholder-active {
|
||||||
border-color: #2563eb;
|
border-color: #1d4ed8;
|
||||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
|
background: linear-gradient(135deg, #e0f2fe 0%, #dbeafe 100%);
|
||||||
|
color: #1e3a8a;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
/* animation: svg-active-pulse 1.6s ease-in-out infinite; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-content-wrapper {
|
.svg-content-wrapper {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<!-- API配置按钮 -->
|
<!-- API配置按钮 -->
|
||||||
<button id="settings-btn" class="settings-btn bg-white/20 text-white p-2 border-2 border-white hover:bg-white/30 transition-all" title="API配置">
|
<button id="settings-btn" class="settings-btn bg-white/20 text-white p-2 border-2 border-white hover:bg-white/30 transition-all" title="API配置">
|
||||||
|
API配置
|
||||||
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
|
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -43,10 +44,10 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<main class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 overflow-hidden">
|
<main class="flex-1 min-h-0 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 overflow-hidden">
|
||||||
|
|
||||||
<!-- 左侧对话面板 -->
|
<!-- 左侧对话面板 -->
|
||||||
<div class="md:col-span-1 bg-white wild-border border-cyan-500 flex flex-col">
|
<div class="md:col-span-1 bg-white wild-border border-cyan-500 flex flex-col min-h-0">
|
||||||
<!-- 对话历史顶部栏 -->
|
<!-- 对话历史顶部栏 -->
|
||||||
<div class="p-3 border-b-3 border-gray-300 bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-between">
|
<div class="p-3 border-b-3 border-gray-300 bg-gradient-to-r from-cyan-500 to-blue-500 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -85,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧显示面板 -->
|
<!-- 右侧显示面板 -->
|
||||||
<div class="md:col-span-2 bg-white wild-border border-purple-600 flex flex-col">
|
<div class="md:col-span-2 bg-white wild-border border-purple-600 flex flex-col min-h-0">
|
||||||
<div id="svg-viewer" class="flex-1 flex items-center justify-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 overflow-auto">
|
<div id="svg-viewer" class="flex-1 flex items-center justify-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 overflow-auto">
|
||||||
<div id="svg-placeholder" class="text-center text-gray-400">
|
<div id="svg-placeholder" class="text-center text-gray-400">
|
||||||
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
|
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
|
||||||
|
|||||||
106
js/app.js
106
js/app.js
@@ -26,6 +26,8 @@ class ProductCanvasApp {
|
|||||||
this.pendingSvgId = null;
|
this.pendingSvgId = null;
|
||||||
this.pendingCancel = false;
|
this.pendingCancel = false;
|
||||||
this.copyClipboardSupported = typeof ClipboardItem !== 'undefined' && !!navigator.clipboard;
|
this.copyClipboardSupported = typeof ClipboardItem !== 'undefined' && !!navigator.clipboard;
|
||||||
|
const deviceScale = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1;
|
||||||
|
this.imageExportScale = Math.min(4, Math.max(2, deviceScale));
|
||||||
|
|
||||||
this.initElements();
|
this.initElements();
|
||||||
this.initEventListeners();
|
this.initEventListeners();
|
||||||
@@ -755,6 +757,14 @@ class ProductCanvasApp {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildDeleteButtonHtml(messageId) {
|
||||||
|
return `
|
||||||
|
<button class="message-delete-btn" title="删除此消息" onclick="event.stopPropagation(); app.confirmDeleteMessage('${messageId}')">
|
||||||
|
<iconify-icon icon="ph:trash-simple-bold"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// 组装标准化的SVG消息字符串
|
// 组装标准化的SVG消息字符串
|
||||||
buildSVGMessageContent(beforeText = '', svgBody = '', afterText = '') {
|
buildSVGMessageContent(beforeText = '', svgBody = '', afterText = '') {
|
||||||
const segments = [];
|
const segments = [];
|
||||||
@@ -857,6 +867,62 @@ class ProductCanvasApp {
|
|||||||
this.renderSvgViewerForMode();
|
this.renderSvgViewerForMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmDeleteMessage(messageId) {
|
||||||
|
const history = this.conversationHistory[this.currentMode] || [];
|
||||||
|
const targetMessage = history.find(msg => msg.id === messageId);
|
||||||
|
if (!targetMessage) {
|
||||||
|
alert('未找到要删除的消息,请刷新后重试。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeLabel = targetMessage.type === 'user'
|
||||||
|
? '这条用户消息'
|
||||||
|
: targetMessage.type === 'ai'
|
||||||
|
? '这条AI回复'
|
||||||
|
: '这条提示';
|
||||||
|
|
||||||
|
const confirmed = confirm(`${typeLabel}删除后无法恢复,确定要删除吗?`);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deleteMessagePermanently(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteMessagePermanently(messageId) {
|
||||||
|
const history = this.conversationHistory[this.currentMode] || [];
|
||||||
|
const messageIndex = history.findIndex(msg => msg.id === messageId);
|
||||||
|
if (messageIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
history.splice(messageIndex, 1);
|
||||||
|
|
||||||
|
const svgStore = this.svgStorage[this.currentMode] || {};
|
||||||
|
let viewerShouldReset = false;
|
||||||
|
for (const [svgId, meta] of Object.entries(svgStore)) {
|
||||||
|
if (meta.messageId === messageId) {
|
||||||
|
delete svgStore[svgId];
|
||||||
|
if (this.currentSvgId === svgId) {
|
||||||
|
viewerShouldReset = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewerShouldReset) {
|
||||||
|
this.currentSvgId = null;
|
||||||
|
this.showSvgPlaceholder();
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.storage.set('canvasHistory', this.conversationHistory.canvas || []);
|
||||||
|
Utils.storage.set('swotHistory', this.conversationHistory.swot || []);
|
||||||
|
Utils.storage.set('canvasSVGs', this.svgStorage.canvas || {});
|
||||||
|
Utils.storage.set('swotSVGs', this.svgStorage.swot || {});
|
||||||
|
|
||||||
|
this.renderConversationHistory();
|
||||||
|
this.renderSvgViewerForMode();
|
||||||
|
}
|
||||||
|
|
||||||
// 添加用户消息
|
// 添加用户消息
|
||||||
addUserMessage(text) {
|
addUserMessage(text) {
|
||||||
const messageId = Utils.generateId('msg');
|
const messageId = Utils.generateId('msg');
|
||||||
@@ -933,25 +999,33 @@ class ProductCanvasApp {
|
|||||||
|
|
||||||
if (message.type === 'user') {
|
if (message.type === 'user') {
|
||||||
messageDiv.className = 'flex justify-end';
|
messageDiv.className = 'flex justify-end';
|
||||||
|
const deleteButton = this.buildDeleteButtonHtml(message.id);
|
||||||
messageDiv.innerHTML = `
|
messageDiv.innerHTML = `
|
||||||
<div class="chat-bubble-user">
|
<div class="chat-bubble-user message-with-delete" data-message-id="${message.id}">
|
||||||
${Utils.escapeHtml(message.content)}
|
<div>${Utils.escapeHtml(message.content)}</div>
|
||||||
|
${deleteButton}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (message.type === 'error') {
|
} else if (message.type === 'error') {
|
||||||
messageDiv.className = 'flex justify-start';
|
messageDiv.className = 'flex justify-start';
|
||||||
|
const deleteButton = this.buildDeleteButtonHtml(message.id);
|
||||||
messageDiv.innerHTML = `
|
messageDiv.innerHTML = `
|
||||||
<div class="chat-bubble-ai border-red-500">
|
<div class="chat-bubble-ai message-with-delete border-red-500" data-message-id="${message.id}">
|
||||||
<iconify-icon icon="ph:warning-circle" class="text-red-500 mr-2"></iconify-icon>
|
${deleteButton}
|
||||||
${Utils.escapeHtml(message.content)}
|
<div class="flex items-start gap-2">
|
||||||
|
<iconify-icon icon="ph:warning-circle" class="text-red-500 mt-0.5"></iconify-icon>
|
||||||
|
<span>${Utils.escapeHtml(message.content)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (message.type === 'ai') {
|
} else if (message.type === 'ai') {
|
||||||
const parsedContent = typeof marked !== 'undefined' ? marked.parse(message.content) : Utils.escapeHtml(message.content);
|
const parsedContent = typeof marked !== 'undefined' ? marked.parse(message.content) : Utils.escapeHtml(message.content);
|
||||||
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
|
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
|
||||||
|
const deleteButton = this.buildDeleteButtonHtml(message.id);
|
||||||
messageDiv.className = 'flex justify-start';
|
messageDiv.className = 'flex justify-start';
|
||||||
messageDiv.innerHTML = `
|
messageDiv.innerHTML = `
|
||||||
<div class="chat-bubble-ai relative" data-message-id="${message.id}">
|
<div class="chat-bubble-ai relative message-with-delete" data-message-id="${message.id}">
|
||||||
|
${deleteButton}
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
${parsedContent}
|
${parsedContent}
|
||||||
</div>
|
</div>
|
||||||
@@ -970,11 +1044,13 @@ class ProductCanvasApp {
|
|||||||
const beforeHtml = parsed.beforeText ? (typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText)) : '';
|
const beforeHtml = parsed.beforeText ? (typeof marked !== 'undefined' ? marked.parse(parsed.beforeText) : Utils.escapeHtml(parsed.beforeText)) : '';
|
||||||
const afterHtml = parsed.afterText ? (typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText)) : '';
|
const afterHtml = parsed.afterText ? (typeof marked !== 'undefined' ? marked.parse(parsed.afterText) : Utils.escapeHtml(parsed.afterText)) : '';
|
||||||
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
|
const actions = this.buildActionToolbar(message.id, { allowRegenerate, allowRollback });
|
||||||
|
const deleteButton = this.buildDeleteButtonHtml(message.id);
|
||||||
|
|
||||||
messageDiv.className = 'flex justify-start';
|
messageDiv.className = 'flex justify-start';
|
||||||
const placeholderClass = this.currentSvgId === svgId ? 'svg-placeholder-block svg-placeholder-active' : 'svg-placeholder-block';
|
const placeholderClass = this.currentSvgId === svgId ? 'svg-placeholder-block svg-placeholder-active' : 'svg-placeholder-block';
|
||||||
messageDiv.innerHTML = `
|
messageDiv.innerHTML = `
|
||||||
<div class="chat-bubble-ai relative" data-message-id="${message.id}">
|
<div class="chat-bubble-ai relative message-with-delete" data-message-id="${message.id}">
|
||||||
|
${deleteButton}
|
||||||
<div>
|
<div>
|
||||||
${beforeHtml}
|
${beforeHtml}
|
||||||
<div class="${placeholderClass}" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
|
<div class="${placeholderClass}" data-svg-id="${svgId}" onclick="app.viewSVG('${svgId}')">
|
||||||
@@ -1214,7 +1290,7 @@ class ProductCanvasApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertSvgToPngBlob(svgContent) {
|
async convertSvgToPngBlob(svgContent, options = {}) {
|
||||||
const { width, height } = this.parseSvgDimensions(svgContent);
|
const { width, height } = this.parseSvgDimensions(svgContent);
|
||||||
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||||
const url = URL.createObjectURL(svgBlob);
|
const url = URL.createObjectURL(svgBlob);
|
||||||
@@ -1222,14 +1298,16 @@ class ProductCanvasApp {
|
|||||||
try {
|
try {
|
||||||
const img = await this.loadImageFromUrl(url);
|
const img = await this.loadImageFromUrl(url);
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const canvasWidth = Math.max(1, img.naturalWidth || width || 1024);
|
const baseWidth = Math.max(1, img.naturalWidth || width || 1024);
|
||||||
const canvasHeight = Math.max(1, img.naturalHeight || height || 768);
|
const baseHeight = Math.max(1, img.naturalHeight || height || 768);
|
||||||
canvas.width = canvasWidth;
|
const preferredScale = options.scale || this.imageExportScale || 1;
|
||||||
canvas.height = canvasHeight;
|
const exportScale = Math.min(4, Math.max(1, preferredScale));
|
||||||
|
canvas.width = Math.round(baseWidth * exportScale);
|
||||||
|
canvas.height = Math.round(baseHeight * exportScale);
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.drawImage(img, 0, 0, canvasWidth, canvasHeight);
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user