Compare commits
5 Commits
6fe5f4175d
...
b0c487a4ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c487a4ef | ||
|
|
cbf59e3450 | ||
|
|
06e1d5ca19 | ||
|
|
64e93d25b8 | ||
|
|
bacafd66dc |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
712
css/style.css
712
css/style.css
@@ -1,351 +1,361 @@
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* 狂野线条效果 */
|
||||
.wild-border {
|
||||
border: 3px solid;
|
||||
box-shadow: 4px 4px 0px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* 切换按钮激活状态 */
|
||||
.mode-btn-active {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.mode-btn-inactive {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 对话气泡样式 */
|
||||
.chat-bubble-user {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 10px 14px;
|
||||
max-width: 80%;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 2px 2px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.chat-bubble-ai {
|
||||
background: #fff;
|
||||
color: #1f2937;
|
||||
padding: 10px 14px;
|
||||
max-width: 85%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 2px solid #10b981;
|
||||
box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* Markdown样式 */
|
||||
.chat-bubble-ai h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai h2 {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai h3 {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai p {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chat-bubble-ai ul, .chat-bubble-ai ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.chat-bubble-ai li {
|
||||
margin: 0.25em 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-bubble-ai code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.chat-bubble-ai pre {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
padding: 0.75em;
|
||||
overflow-x: auto;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.chat-bubble-ai pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai blockquote {
|
||||
border-left: 4px solid #d1d5db;
|
||||
padding-left: 1em;
|
||||
margin: 0.5em 0;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chat-bubble-ai strong {
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai em {
|
||||
font-style: italic;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.chat-bubble-ai a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.chat-bubble-ai a:hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.chat-bubble-ai table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.chat-bubble-ai th, .chat-bubble-ai td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat-bubble-ai th {
|
||||
background: #f9fafb;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* SVG占位符样式 - 块级换行 + 新配色 */
|
||||
.svg-placeholder-block {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
margin: 8px 0;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.svg-placeholder-block:hover {
|
||||
transform: translateX(2px) translateY(-2px);
|
||||
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f87171 100%);
|
||||
}
|
||||
|
||||
/* SVG绘制中状态占位符 */
|
||||
.svg-drawing-placeholder {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
margin: 8px 0;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-drawing-placeholder:hover {
|
||||
transform: translateX(2px) translateY(-2px);
|
||||
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
|
||||
}
|
||||
|
||||
/* 绘制中动画效果 */
|
||||
@keyframes drawing-pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.svg-drawing-placeholder::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: drawing-shine 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes drawing-shine {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
.svg-drawing-text {
|
||||
animation: drawing-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* 气泡操作按钮 */
|
||||
.bubble-action-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.chat-bubble-ai:hover .bubble-action-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 小手摇摆动画 */
|
||||
@keyframes wave {
|
||||
0%, 100% {transform: translateX(0px) rotate(90deg);}
|
||||
10%, 30%, 50%, 70%, 90% {transform: translateX(-1px) rotate(90deg);}
|
||||
20%, 40%, 60%, 80% {transform: translateX(1px) rotate(90deg);}
|
||||
}
|
||||
|
||||
.wave-hand {
|
||||
animation: wave 3s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* 模态窗样式 */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border: 4px solid #000;
|
||||
box-shadow: 8px 8px 0 rgba(0,0,0,0.4);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 表单输入框样式 */
|
||||
.config-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #000;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.config-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 3px 3px 0 rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 齿轮旋转动画 */
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.settings-btn:hover iconify-icon {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 自适应高度输入框 */
|
||||
.auto-resize-input {
|
||||
min-height: 40px;
|
||||
max-height: 120px; /* 5行左右的高度 */
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* 清空按钮样式 */
|
||||
.clear-btn {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 流式输出动画 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.streaming-text {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 打字机效果光标 */
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.typing-cursor::after {
|
||||
content: '|';
|
||||
animation: blink 1s infinite;
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 清空历史按钮摇动动画 */
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(2px); }
|
||||
}
|
||||
|
||||
.clear-history-btn:hover iconify-icon {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* 狂野线条效果 */
|
||||
.wild-border {
|
||||
border: 3px solid;
|
||||
box-shadow: 4px 4px 0px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* 切换按钮激活状态 */
|
||||
.mode-btn-active {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.mode-btn-inactive {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 对话气泡样式 */
|
||||
.chat-bubble-user {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 10px 14px;
|
||||
max-width: 80%;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 2px 2px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.chat-bubble-ai {
|
||||
background: #fff;
|
||||
color: #1f2937;
|
||||
padding: 10px 14px;
|
||||
max-width: 85%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 2px solid #10b981;
|
||||
box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* Markdown样式 */
|
||||
.chat-bubble-ai h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai h2 {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai h3 {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai p {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chat-bubble-ai ul, .chat-bubble-ai ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.chat-bubble-ai li {
|
||||
margin: 0.25em 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-bubble-ai code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
.chat-bubble-ai pre {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
padding: 0.75em;
|
||||
overflow-x: auto;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.chat-bubble-ai pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai blockquote {
|
||||
border-left: 4px solid #d1d5db;
|
||||
padding-left: 1em;
|
||||
margin: 0.5em 0;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chat-bubble-ai strong {
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.chat-bubble-ai em {
|
||||
font-style: italic;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.chat-bubble-ai a {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.chat-bubble-ai a:hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.chat-bubble-ai table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.chat-bubble-ai th, .chat-bubble-ai td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat-bubble-ai th {
|
||||
background: #f9fafb;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* SVG占位符样式 - 块级换行 + 新配色 */
|
||||
.svg-placeholder-block {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
margin: 8px 0;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.svg-placeholder-block:hover {
|
||||
transform: translateX(2px) translateY(-2px);
|
||||
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
||||
background: linear-gradient(135deg, #fb923c 0%, #f87171 100%);
|
||||
}
|
||||
|
||||
/* SVG绘制中状态占位符 */
|
||||
.svg-drawing-placeholder {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
margin: 8px 0;
|
||||
border: 2px solid #000;
|
||||
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.svg-drawing-placeholder:hover {
|
||||
transform: translateX(2px) translateY(-2px);
|
||||
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
||||
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
|
||||
}
|
||||
|
||||
/* 绘制中动画效果 */
|
||||
@keyframes drawing-pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.svg-drawing-placeholder::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: drawing-shine 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes drawing-shine {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
.svg-drawing-text {
|
||||
animation: drawing-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* 气泡操作按钮 */
|
||||
.bubble-action-btn {
|
||||
opacity: 1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.svg-placeholder-active {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.svg-content-wrapper {
|
||||
display: inline-block;
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
/* 小手摇摆动画 */
|
||||
@keyframes wave {
|
||||
0%, 100% {transform: translateX(0px) rotate(90deg);}
|
||||
10%, 30%, 50%, 70%, 90% {transform: translateX(-1px) rotate(90deg);}
|
||||
20%, 40%, 60%, 80% {transform: translateX(1px) rotate(90deg);}
|
||||
}
|
||||
|
||||
.wave-hand {
|
||||
animation: wave 3s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* 模态窗样式 */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border: 4px solid #000;
|
||||
box-shadow: 8px 8px 0 rgba(0,0,0,0.4);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 表单输入框样式 */
|
||||
.config-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #000;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.config-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 3px 3px 0 rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 齿轮旋转动画 */
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.settings-btn:hover iconify-icon {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 自适应高度输入框 */
|
||||
.auto-resize-input {
|
||||
min-height: 40px;
|
||||
max-height: 120px; /* 5行左右的高度 */
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* 清空按钮样式 */
|
||||
.clear-btn {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.clear-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 流式输出动画 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.streaming-text {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 打字机效果光标 */
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.typing-cursor::after {
|
||||
content: '|';
|
||||
animation: blink 1s infinite;
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 清空历史按钮摇动动画 */
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(2px); }
|
||||
}
|
||||
|
||||
.clear-history-btn:hover iconify-icon {
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
#send-button.terminate-mode {
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
410
index.html
410
index.html
@@ -1,199 +1,211 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>产品画布 / SWOT分析</title>
|
||||
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
|
||||
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body class="bg-gray-100 h-screen flex flex-col">
|
||||
|
||||
<!-- 顶部标题栏 -->
|
||||
<header class="bg-gradient-to-r from-orange-500 to-pink-500 p-3 flex items-center justify-between border-b-4 border-black">
|
||||
<div class="flex items-center space-x-2">
|
||||
<iconify-icon icon="ph:lightning-fill" class="text-3xl text-white"></iconify-icon>
|
||||
<h1 id="page-title" class="text-2xl font-black text-white tracking-tight">产品画布</h1>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮组 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- API配置按钮 -->
|
||||
<button id="settings-btn" class="settings-btn bg-white/20 text-white p-2 border-2 border-white hover:bg-white/30 transition-all" title="API配置">
|
||||
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
|
||||
</button>
|
||||
|
||||
<span class="text-white font-bold text-sm">点击切换模式</span>
|
||||
<iconify-icon icon="ph:hand-pointing-fill" class="text-2xl text-yellow-300 wave-hand"></iconify-icon>
|
||||
|
||||
<button id="canvas-mode-btn" class="mode-btn-active bg-white text-orange-600 px-4 py-2 font-bold border-2 border-black hover:bg-orange-50 transition-all duration-200">
|
||||
<iconify-icon icon="ph:pen-nib-duotone" class="align-middle mr-1"></iconify-icon>
|
||||
产品画布
|
||||
</button>
|
||||
<button id="swot-mode-btn" class="mode-btn-inactive bg-white text-purple-600 px-4 py-2 font-bold border-2 border-black hover:bg-purple-50 transition-all duration-200">
|
||||
<iconify-icon icon="ph:chart-bar-duotone" class="align-middle mr-1"></iconify-icon>
|
||||
SWOT分析
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 overflow-hidden">
|
||||
|
||||
<!-- 左侧对话面板 -->
|
||||
<div class="md:col-span-1 bg-white wild-border border-cyan-500 flex flex-col">
|
||||
<!-- 对话历史顶部栏 -->
|
||||
<div 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">
|
||||
<iconify-icon icon="ph:chats-circle-fill" class="text-2xl text-white"></iconify-icon>
|
||||
<span class="font-black text-white">对话历史</span>
|
||||
</div>
|
||||
<button id="clear-history-btn" class="clear-history-btn bg-red-500 text-white px-3 py-1 border-2 border-black hover:bg-red-600 transition-all flex items-center gap-1 font-bold" title="清空对话历史">
|
||||
<iconify-icon icon="ph:trash-bold" class="text-lg"></iconify-icon>
|
||||
<span class="text-sm">清空</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 对话历史区 -->
|
||||
<div id="chat-history" class="flex-1 p-4 overflow-y-auto space-y-3">
|
||||
<!-- 欢迎消息 -->
|
||||
<div class="flex justify-start">
|
||||
<div class="chat-bubble-ai">
|
||||
👋 欢迎使用产品画布/SWOT分析工具!请输入您的需求,我将为您生成专业的分析图表。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<textarea
|
||||
id="chat-input"
|
||||
placeholder="输入您的想法,按Enter发送,Shift+Enter换行..."
|
||||
class="flex-1 auto-resize-input border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
|
||||
rows="1"
|
||||
></textarea>
|
||||
<button id="send-button" class="text-cyan-600 hover:text-cyan-700 transition-colors p-2 hover:scale-110 transform duration-200">
|
||||
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧显示面板 -->
|
||||
<div class="md:col-span-2 bg-white wild-border border-purple-600 flex flex-col">
|
||||
<div id="svg-viewer" class="flex-1 flex items-center justify-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 overflow-auto">
|
||||
<div id="svg-placeholder" class="text-center text-gray-400">
|
||||
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
|
||||
<p class="mt-2 font-bold" id="placeholder-text">生成的产品画布将在此处显示</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="p-3 border-t-3 border-gray-300 flex justify-end items-center gap-2 bg-gray-800">
|
||||
<button id="download-svg-btn" class="p-2 bg-orange-500 text-white border-2 border-black hover:bg-orange-600 transition-all" title="下载SVG">
|
||||
<iconify-icon icon="mdi:download-outline" class="text-xl"></iconify-icon>
|
||||
</button>
|
||||
<button id="export-image-btn" class="p-2 bg-green-500 text-white border-2 border-black hover:bg-green-600 transition-all" title="导出为图片">
|
||||
<iconify-icon icon="mdi:image-outline" class="text-xl"></iconify-icon>
|
||||
</button>
|
||||
<button id="view-code-btn" class="p-2 bg-blue-500 text-white border-2 border-black hover:bg-blue-600 transition-all" title="查看代码">
|
||||
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- API配置模态窗 -->
|
||||
<div id="config-modal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<!-- 模态窗头部 -->
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-4 border-b-4 border-black flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<iconify-icon icon="ph:plugs-connected-fill" class="text-3xl text-white"></iconify-icon>
|
||||
<h2 class="text-xl font-black text-white">API 配置</h2>
|
||||
</div>
|
||||
<button id="close-modal-btn" class="text-white hover:bg-white/20 p-2 transition-all">
|
||||
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 模态窗内容 -->
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- API URL -->
|
||||
<div>
|
||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<iconify-icon icon="ph:link-bold" class="text-lg text-blue-600"></iconify-icon>
|
||||
API URL
|
||||
</label>
|
||||
<input
|
||||
id="api-url"
|
||||
type="text"
|
||||
placeholder="https://api.example.com/v1/chat"
|
||||
class="config-input"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- API Key -->
|
||||
<div>
|
||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<iconify-icon icon="ph:key-bold" class="text-lg text-green-600"></iconify-icon>
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
id="api-key"
|
||||
type="password"
|
||||
placeholder="sk-xxxxxxxxxxxxxxxx"
|
||||
class="config-input"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Model -->
|
||||
<div>
|
||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<iconify-icon icon="ph:robot-bold" class="text-lg text-purple-600"></iconify-icon>
|
||||
模型 (Model)
|
||||
</label>
|
||||
<input
|
||||
id="api-model"
|
||||
type="text"
|
||||
placeholder="gpt-4-turbo"
|
||||
class="config-input"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 状态显示 -->
|
||||
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
|
||||
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||
<span id="status-text">等待操作...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模态窗底部按钮 -->
|
||||
<div class="p-4 border-t-3 border-gray-300 bg-gray-100 flex gap-3 justify-end">
|
||||
<button id="test-api-btn" class="px-4 py-2 bg-yellow-500 text-white font-bold border-2 border-black hover:bg-yellow-600 transition-all flex items-center gap-2">
|
||||
<iconify-icon icon="ph:flask-bold"></iconify-icon>
|
||||
测试连接
|
||||
</button>
|
||||
<button id="save-config-btn" class="px-4 py-2 bg-green-500 text-white font-bold border-2 border-black hover:bg-green-600 transition-all flex items-center gap-2">
|
||||
<iconify-icon icon="ph:floppy-disk-bold"></iconify-icon>
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引入JavaScript文件 -->
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/apiclient.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>产品画布 / SWOT分析</title>
|
||||
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
|
||||
<script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body class="bg-gray-100 h-screen flex flex-col">
|
||||
|
||||
<!-- 顶部标题栏 -->
|
||||
<header class="bg-gradient-to-r from-orange-500 to-pink-500 p-3 flex items-center justify-between border-b-4 border-black">
|
||||
<div class="flex items-center space-x-2">
|
||||
<iconify-icon icon="ph:lightning-fill" class="text-3xl text-white"></iconify-icon>
|
||||
<h1 id="page-title" class="text-2xl font-black text-white tracking-tight">产品画布</h1>
|
||||
</div>
|
||||
|
||||
<!-- 右侧按钮组 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- API配置按钮 -->
|
||||
<button id="settings-btn" class="settings-btn bg-white/20 text-white p-2 border-2 border-white hover:bg-white/30 transition-all" title="API配置">
|
||||
<iconify-icon icon="ph:gear-six-fill" class="text-2xl"></iconify-icon>
|
||||
</button>
|
||||
|
||||
<span class="text-white font-bold text-sm">点击切换模式</span>
|
||||
<iconify-icon icon="ph:hand-pointing-fill" class="text-2xl text-yellow-300 wave-hand"></iconify-icon>
|
||||
|
||||
<button id="canvas-mode-btn" class="mode-btn-active bg-white text-orange-600 px-4 py-2 font-bold border-2 border-black hover:bg-orange-50 transition-all duration-200">
|
||||
<iconify-icon icon="ph:pen-nib-duotone" class="align-middle mr-1"></iconify-icon>
|
||||
产品画布
|
||||
</button>
|
||||
<button id="swot-mode-btn" class="mode-btn-inactive bg-white text-purple-600 px-4 py-2 font-bold border-2 border-black hover:bg-purple-50 transition-all duration-200">
|
||||
<iconify-icon icon="ph:chart-bar-duotone" class="align-middle mr-1"></iconify-icon>
|
||||
SWOT分析
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 overflow-hidden">
|
||||
|
||||
<!-- 左侧对话面板 -->
|
||||
<div class="md:col-span-1 bg-white wild-border border-cyan-500 flex flex-col">
|
||||
<!-- 对话历史顶部栏 -->
|
||||
<div 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">
|
||||
<iconify-icon icon="ph:chats-circle-fill" class="text-2xl text-white"></iconify-icon>
|
||||
<span class="font-black text-white">对话历史</span>
|
||||
</div>
|
||||
<button id="clear-history-btn" class="clear-history-btn bg-red-500 text-white px-3 py-1 border-2 border-black hover:bg-red-600 transition-all flex items-center gap-1 font-bold" title="清空对话历史">
|
||||
<iconify-icon icon="ph:trash-bold" class="text-lg"></iconify-icon>
|
||||
<span class="text-sm">清空</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 对话历史区 -->
|
||||
<div id="chat-history" class="flex-1 p-4 overflow-y-auto space-y-3">
|
||||
<!-- 欢迎消息 -->
|
||||
<div class="flex justify-start">
|
||||
<div class="chat-bubble-ai">
|
||||
👋 欢迎使用产品画布/SWOT分析工具!请输入您的需求,我将为您生成专业的分析图表。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
|
||||
<div class="relative flex items-center gap-2">
|
||||
<textarea
|
||||
id="chat-input"
|
||||
placeholder="输入您的想法,按Enter发送,Shift+Enter换行..."
|
||||
class="flex-1 auto-resize-input border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
|
||||
rows="1"
|
||||
></textarea>
|
||||
<button id="send-button" class="text-cyan-600 hover:text-cyan-700 transition-colors p-2 hover:scale-110 transform duration-200">
|
||||
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧显示面板 -->
|
||||
<div class="md:col-span-2 bg-white wild-border border-purple-600 flex flex-col">
|
||||
<div id="svg-viewer" class="flex-1 flex items-center justify-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 overflow-auto">
|
||||
<div id="svg-placeholder" class="text-center text-gray-400">
|
||||
<iconify-icon icon="ph:image-square" class="text-6xl mx-auto text-purple-400"></iconify-icon>
|
||||
<p class="mt-2 font-bold" id="placeholder-text">生成的产品画布将在此处显示</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="p-3 border-t-3 border-gray-300 flex justify-end items-center gap-2 bg-gray-800">
|
||||
<button id="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>
|
||||
<button id="view-code-btn" class="p-2 bg-blue-500 text-white border-2 border-black hover:bg-blue-600 transition-all" title="查看代码">
|
||||
<iconify-icon icon="mdi:code-tags" class="text-xl"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- API配置模态窗 -->
|
||||
<div id="config-modal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<!-- 模态窗头部 -->
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 p-4 border-b-4 border-black flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<iconify-icon icon="ph:plugs-connected-fill" class="text-3xl text-white"></iconify-icon>
|
||||
<h2 class="text-xl font-black text-white">API 配置</h2>
|
||||
</div>
|
||||
<button id="close-modal-btn" class="text-white hover:bg-white/20 p-2 transition-all">
|
||||
<iconify-icon icon="ph:x-bold" class="text-2xl"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 模态窗内容 -->
|
||||
<div class="p-6 space-y-4">
|
||||
<!-- API URL -->
|
||||
<div>
|
||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<iconify-icon icon="ph:link-bold" class="text-lg text-blue-600"></iconify-icon>
|
||||
API URL
|
||||
</label>
|
||||
<input
|
||||
id="api-url"
|
||||
type="text"
|
||||
placeholder="https://api.example.com/v1/chat"
|
||||
class="config-input"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- API Key -->
|
||||
<div>
|
||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<iconify-icon icon="ph:key-bold" class="text-lg text-green-600"></iconify-icon>
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
id="api-key"
|
||||
type="password"
|
||||
placeholder="sk-xxxxxxxxxxxxxxxx"
|
||||
class="config-input"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Model -->
|
||||
<div>
|
||||
<label class="block font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<iconify-icon icon="ph:robot-bold" class="text-lg text-purple-600"></iconify-icon>
|
||||
模型 (Model)
|
||||
</label>
|
||||
<input
|
||||
id="api-model"
|
||||
type="text"
|
||||
placeholder="gpt-4-turbo"
|
||||
class="config-input"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 状态显示 -->
|
||||
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
|
||||
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||
<span id="status-text">等待操作...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模态窗底部按钮 -->
|
||||
<div class="p-4 border-t-3 border-gray-300 bg-gray-100 flex gap-3 justify-end">
|
||||
<button id="test-api-btn" class="px-4 py-2 bg-yellow-500 text-white font-bold border-2 border-black hover:bg-yellow-600 transition-all flex items-center gap-2">
|
||||
<iconify-icon icon="ph:flask-bold"></iconify-icon>
|
||||
测试连接
|
||||
</button>
|
||||
<button id="save-config-btn" class="px-4 py-2 bg-green-500 text-white font-bold border-2 border-black hover:bg-green-600 transition-all flex items-center gap-2">
|
||||
<iconify-icon icon="ph:floppy-disk-bold"></iconify-icon>
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引入JavaScript文件 -->
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/apiclient.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
596
js/apiclient.js
596
js/apiclient.js
@@ -1,299 +1,299 @@
|
||||
/**
|
||||
* API客户端 - 处理与AI服务的交互
|
||||
*/
|
||||
|
||||
class APIClient {
|
||||
constructor() {
|
||||
this.config = {
|
||||
url: '',
|
||||
key: '',
|
||||
model: ''
|
||||
};
|
||||
this.prompts = {
|
||||
canvas: '',
|
||||
swot: ''
|
||||
};
|
||||
this.loadConfig();
|
||||
this.loadPrompts();
|
||||
}
|
||||
|
||||
// 加载API配置
|
||||
loadConfig() {
|
||||
const savedConfig = Utils.storage.get('apiConfig');
|
||||
if (savedConfig) {
|
||||
this.config = { ...this.config, ...savedConfig };
|
||||
}
|
||||
}
|
||||
|
||||
// 加载系统提示词
|
||||
async loadPrompts() {
|
||||
try {
|
||||
// 加载产品画布提示词
|
||||
const canvasResponse = await fetch('prompts/canvas-prompt.txt');
|
||||
this.prompts.canvas = await canvasResponse.text();
|
||||
|
||||
// 加载SWOT分析提示词
|
||||
const swotResponse = await fetch('prompts/swot-prompt.txt');
|
||||
this.prompts.swot = await swotResponse.text();
|
||||
} catch (error) {
|
||||
console.error('加载提示词失败:', error);
|
||||
// 使用默认提示词
|
||||
this.prompts.canvas = '你是一个专业的产品战略分析师,擅长创建产品画布。';
|
||||
this.prompts.swot = '你是一个专业的商业战略分析师,擅长进行SWOT分析。';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存API配置
|
||||
saveConfig(config) {
|
||||
this.config = { ...this.config, ...config };
|
||||
return Utils.storage.set('apiConfig', this.config);
|
||||
}
|
||||
|
||||
// 获取当前配置
|
||||
getConfig() {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
// 验证配置是否完整
|
||||
isConfigValid() {
|
||||
return this.config.url && this.config.key && this.config.model;
|
||||
}
|
||||
|
||||
// 测试API连接
|
||||
async testConnection() {
|
||||
if (!this.isConfigValid()) {
|
||||
throw new Error('API配置不完整,请填写所有字段');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest([
|
||||
{ role: 'user', content: '测试连接' }
|
||||
], 5);
|
||||
|
||||
return { success: true, data: response };
|
||||
} catch (error) {
|
||||
throw new Error(`连接测试失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送聊天请求
|
||||
async sendChatMessage(messages, options = {}) {
|
||||
if (!this.isConfigValid()) {
|
||||
throw new Error('API配置不完整,请先配置API设置');
|
||||
}
|
||||
|
||||
const maxTokens = options.maxTokens || 2000;
|
||||
const temperature = options.temperature || 0.7;
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest(messages, maxTokens, temperature);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`API请求失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 核心请求方法
|
||||
async makeRequest(messages, maxTokens, temperature = 0.7) {
|
||||
const requestBody = {
|
||||
model: this.config.model,
|
||||
messages: messages,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature
|
||||
};
|
||||
|
||||
const response = await fetch(this.config.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.config.key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.choices || !data.choices.length || !data.choices[0].message) {
|
||||
throw new Error('API返回数据格式异常');
|
||||
}
|
||||
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
// 生成产品画布的专用方法
|
||||
async generateProductCanvas(userRequest, context = []) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.canvas },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return await this.sendChatMessage(messages, { maxTokens: 3000 });
|
||||
}
|
||||
|
||||
// 生成SWOT分析的专用方法
|
||||
async generateSWOTAnalysis(userRequest, context = []) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.swot },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return await this.sendChatMessage(messages, { maxTokens: 3000 });
|
||||
}
|
||||
|
||||
// 流式生成产品画布
|
||||
async generateProductCanvasStream(userRequest, context = [], onChunk, onComplete) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.canvas },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete);
|
||||
}
|
||||
|
||||
// 流式生成SWOT分析
|
||||
async generateSWOTAnalysisStream(userRequest, context = [], onChunk, onComplete) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.swot },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return await this.sendChatMessageStream(messages, { maxTokens: 3000 }, onChunk, onComplete);
|
||||
}
|
||||
|
||||
// 流式发送聊天请求
|
||||
async sendChatMessageStream(messages, options = {}, onChunk, onComplete) {
|
||||
if (!this.isConfigValid()) {
|
||||
throw new Error('API配置不完整,请先配置API设置');
|
||||
}
|
||||
|
||||
const maxTokens = options.maxTokens || 2000;
|
||||
const temperature = options.temperature || 0.7;
|
||||
const stream = true;
|
||||
|
||||
const requestBody = {
|
||||
model: this.config.model,
|
||||
messages: messages,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
stream: stream
|
||||
};
|
||||
|
||||
const url = this.config.url.replace('/chat/completions', '/chat/completions');
|
||||
|
||||
try {
|
||||
await Utils.createStreamRequest(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.config.key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
},
|
||||
onChunk,
|
||||
onComplete
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`流式API请求失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 重新生成响应
|
||||
async regenerateResponse(messageId, conversationHistory) {
|
||||
// 找到指定消息ID之前的所有对话历史
|
||||
const contextMessages = conversationHistory
|
||||
.filter(msg => msg.id <= messageId)
|
||||
.map(msg => ({
|
||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
|
||||
if (contextMessages.length === 0) {
|
||||
throw new Error('没有找到有效的对话上下文');
|
||||
}
|
||||
|
||||
// 移除最后一条消息(需要重新生成的消息)
|
||||
if (contextMessages.length > 0 && contextMessages[contextMessages.length - 1].role === 'assistant') {
|
||||
contextMessages.pop();
|
||||
}
|
||||
|
||||
// 根据当前模式选择相应的生成方法
|
||||
const lastUserMessage = contextMessages.filter(msg => msg.role === 'user').pop();
|
||||
if (!lastUserMessage) {
|
||||
throw new Error('没有找到用户消息');
|
||||
}
|
||||
|
||||
const mode = Utils.storage.get('currentMode', 'canvas');
|
||||
|
||||
if (mode === 'canvas') {
|
||||
return await this.generateProductCanvas(lastUserMessage.content, contextMessages.slice(0, -1));
|
||||
} else {
|
||||
return await this.generateSWOTAnalysis(lastUserMessage.content, contextMessages.slice(0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟API响应(用于测试)
|
||||
simulateAPIResponse(userMessage, mode = 'canvas') {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const mockResponses = [
|
||||
`好的!我为您生成了一个${mode === 'canvas' ? '产品画布' : 'SWOT分析'}
|
||||
\`\`\`svg
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
||||
<rect x="50" y="50" width="500" height="300" fill="url(#grad1)" rx="10"/>
|
||||
<text x="300" y="200" text-anchor="middle" font-size="24" fill="white" font-weight="bold">
|
||||
这是${mode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG
|
||||
</text>
|
||||
<circle cx="150" cy="150" r="40" fill="#ffffff" opacity="0.3"/>
|
||||
<circle cx="450" cy="250" r="30" fill="#ffffff" opacity="0.3"/>
|
||||
</svg>
|
||||
\`\`\`
|
||||
包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
|
||||
|
||||
`已经为您调整完成!
|
||||
\`\`\`svg
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#ef4444;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#fff"/>
|
||||
<rect x="75" y="75" width="450" height="250" fill="url(#grad2)" rx="15"/>
|
||||
<text x="300" y="200" text-anchor="middle" font-size="28" fill="white" font-weight="bold">
|
||||
${mode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'}
|
||||
</text>
|
||||
<rect x="100" y="120" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
|
||||
<rect x="420" y="220" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
|
||||
</svg>
|
||||
\`\`\`
|
||||
采用了更加鲜明的色彩组合,希望您满意!`
|
||||
];
|
||||
|
||||
const response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
|
||||
resolve(response);
|
||||
}, 1000 + Math.random() * 1000); // 1-2秒的随机延迟
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局API客户端实例
|
||||
/**
|
||||
* API客户端 - 处理与AI服务的交互
|
||||
*/
|
||||
|
||||
class APIClient {
|
||||
constructor() {
|
||||
this.config = {
|
||||
url: '',
|
||||
key: '',
|
||||
model: ''
|
||||
};
|
||||
this.prompts = {
|
||||
canvas: '',
|
||||
swot: ''
|
||||
};
|
||||
this.loadConfig();
|
||||
this.loadPrompts();
|
||||
}
|
||||
|
||||
// 加载API配置
|
||||
loadConfig() {
|
||||
const savedConfig = Utils.storage.get('apiConfig');
|
||||
if (savedConfig) {
|
||||
this.config = { ...this.config, ...savedConfig };
|
||||
}
|
||||
}
|
||||
|
||||
// 加载系统提示词
|
||||
async loadPrompts() {
|
||||
try {
|
||||
// 加载产品画布提示词
|
||||
const canvasResponse = await fetch('prompts/canvas-prompt.txt');
|
||||
this.prompts.canvas = await canvasResponse.text();
|
||||
|
||||
// 加载SWOT分析提示词
|
||||
const swotResponse = await fetch('prompts/swot-prompt.txt');
|
||||
this.prompts.swot = await swotResponse.text();
|
||||
} catch (error) {
|
||||
console.error('加载提示词失败:', error);
|
||||
// 使用默认提示词
|
||||
this.prompts.canvas = '你是一个专业的产品战略分析师,擅长创建产品画布。';
|
||||
this.prompts.swot = '你是一个专业的商业战略分析师,擅长进行SWOT分析。';
|
||||
}
|
||||
}
|
||||
|
||||
// 保存API配置
|
||||
saveConfig(config) {
|
||||
this.config = { ...this.config, ...config };
|
||||
return Utils.storage.set('apiConfig', this.config);
|
||||
}
|
||||
|
||||
// 获取当前配置
|
||||
getConfig() {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
// 验证配置是否完整
|
||||
isConfigValid() {
|
||||
return this.config.url && this.config.key && this.config.model;
|
||||
}
|
||||
|
||||
// 测试API连接
|
||||
async testConnection() {
|
||||
if (!this.isConfigValid()) {
|
||||
throw new Error('API配置不完整,请填写所有字段');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest([
|
||||
{ role: 'user', content: '测试连接' }
|
||||
], 5);
|
||||
|
||||
return { success: true, data: response };
|
||||
} catch (error) {
|
||||
throw new Error(`连接测试失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送聊天请求
|
||||
async sendChatMessage(messages, options = {}) {
|
||||
if (!this.isConfigValid()) {
|
||||
throw new Error('API配置不完整,请先配置API设置');
|
||||
}
|
||||
|
||||
const maxTokens = options.maxTokens || 2000;
|
||||
const temperature = options.temperature || 0.7;
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest(messages, maxTokens, temperature);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`API请求失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 核心请求方法
|
||||
async makeRequest(messages, maxTokens, temperature = 0.7) {
|
||||
const requestBody = {
|
||||
model: this.config.model,
|
||||
messages: messages,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature
|
||||
};
|
||||
|
||||
const response = await fetch(this.config.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.config.key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.choices || !data.choices.length || !data.choices[0].message) {
|
||||
throw new Error('API返回数据格式异常');
|
||||
}
|
||||
|
||||
return data.choices[0].message.content;
|
||||
}
|
||||
|
||||
// 生成产品画布的专用方法
|
||||
async generateProductCanvas(userRequest, context = []) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.canvas },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return await this.sendChatMessage(messages, { maxTokens: 18000 });
|
||||
}
|
||||
|
||||
// 生成SWOT分析的专用方法
|
||||
async generateSWOTAnalysis(userRequest, context = []) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.swot },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return await this.sendChatMessage(messages, { maxTokens: 18000 });
|
||||
}
|
||||
|
||||
// 流式生成产品画布
|
||||
async generateProductCanvasStream(userRequest, context = [], onChunk, onComplete) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.canvas },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
|
||||
}
|
||||
|
||||
// 流式生成SWOT分析
|
||||
async generateSWOTAnalysisStream(userRequest, context = [], onChunk, onComplete) {
|
||||
const messages = [
|
||||
{ role: 'system', content: this.prompts.swot },
|
||||
...context,
|
||||
{ role: 'user', content: userRequest }
|
||||
];
|
||||
|
||||
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
|
||||
}
|
||||
|
||||
// 流式发送聊天请求
|
||||
async sendChatMessageStream(messages, options = {}, onChunk, onComplete) {
|
||||
if (!this.isConfigValid()) {
|
||||
throw new Error('API配置不完整,请先配置API设置');
|
||||
}
|
||||
|
||||
const maxTokens = options.maxTokens || 2000;
|
||||
const temperature = options.temperature || 0.7;
|
||||
const stream = true;
|
||||
|
||||
const requestBody = {
|
||||
model: this.config.model,
|
||||
messages: messages,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
stream: stream
|
||||
};
|
||||
|
||||
const url = this.config.url.replace('/chat/completions', '/chat/completions');
|
||||
|
||||
try {
|
||||
return Utils.createStreamRequest(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.config.key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
},
|
||||
onChunk,
|
||||
onComplete
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`流式API请求失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 重新生成响应
|
||||
async regenerateResponse(messageId, conversationHistory) {
|
||||
// 找到指定消息ID之前的所有对话历史
|
||||
const contextMessages = conversationHistory
|
||||
.filter(msg => msg.id <= messageId)
|
||||
.map(msg => ({
|
||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content
|
||||
}));
|
||||
|
||||
if (contextMessages.length === 0) {
|
||||
throw new Error('没有找到有效的对话上下文');
|
||||
}
|
||||
|
||||
// 移除最后一条消息(需要重新生成的消息)
|
||||
if (contextMessages.length > 0 && contextMessages[contextMessages.length - 1].role === 'assistant') {
|
||||
contextMessages.pop();
|
||||
}
|
||||
|
||||
// 根据当前模式选择相应的生成方法
|
||||
const lastUserMessage = contextMessages.filter(msg => msg.role === 'user').pop();
|
||||
if (!lastUserMessage) {
|
||||
throw new Error('没有找到用户消息');
|
||||
}
|
||||
|
||||
const mode = Utils.storage.get('currentMode', 'canvas');
|
||||
|
||||
if (mode === 'canvas') {
|
||||
return await this.generateProductCanvas(lastUserMessage.content, contextMessages.slice(0, -1));
|
||||
} else {
|
||||
return await this.generateSWOTAnalysis(lastUserMessage.content, contextMessages.slice(0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟API响应(用于测试)
|
||||
simulateAPIResponse(userMessage, mode = 'canvas') {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const mockResponses = [
|
||||
`好的!我为您生成了一个${mode === 'canvas' ? '产品画布' : 'SWOT分析'}
|
||||
\`\`\`svg
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
||||
<rect x="50" y="50" width="500" height="300" fill="url(#grad1)" rx="10"/>
|
||||
<text x="300" y="200" text-anchor="middle" font-size="24" fill="white" font-weight="bold">
|
||||
这是${mode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG
|
||||
</text>
|
||||
<circle cx="150" cy="150" r="40" fill="#ffffff" opacity="0.3"/>
|
||||
<circle cx="450" cy="250" r="30" fill="#ffffff" opacity="0.3"/>
|
||||
</svg>
|
||||
\`\`\`
|
||||
包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
|
||||
|
||||
`已经为您调整完成!
|
||||
\`\`\`svg
|
||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#ef4444;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="#fff"/>
|
||||
<rect x="75" y="75" width="450" height="250" fill="url(#grad2)" rx="15"/>
|
||||
<text x="300" y="200" text-anchor="middle" font-size="28" fill="white" font-weight="bold">
|
||||
${mode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'}
|
||||
</text>
|
||||
<rect x="100" y="120" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
|
||||
<rect x="420" y="220" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
|
||||
</svg>
|
||||
\`\`\`
|
||||
采用了更加鲜明的色彩组合,希望您满意!`
|
||||
];
|
||||
|
||||
const response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
|
||||
resolve(response);
|
||||
}, 1000 + Math.random() * 1000); // 1-2秒的随机延迟
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局API客户端实例
|
||||
window.apiClient = new APIClient();
|
||||
641
js/utils.js
641
js/utils.js
@@ -1,289 +1,352 @@
|
||||
/**
|
||||
* 工具函数集合
|
||||
*/
|
||||
|
||||
// HTML转义,防止XSS攻击
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 滚动到指定元素的底部
|
||||
function scrollToBottom(element) {
|
||||
if (element) {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
function generateId(prefix = 'id') {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
// 解析SVG响应,提取SVG内容和前后文本
|
||||
function parseSVGResponse(response) {
|
||||
const svgRegex = /```svg\s*([\s\S]*?)```/i;
|
||||
const match = response.match(svgRegex);
|
||||
|
||||
if (match) {
|
||||
const svgContent = match[1].trim();
|
||||
const beforeText = response.substring(0, match.index).trim();
|
||||
const afterText = response.substring(match.index + match[0].length).trim();
|
||||
|
||||
return {
|
||||
svgContent,
|
||||
beforeText,
|
||||
afterText
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
svgContent: null,
|
||||
beforeText: response,
|
||||
afterText: ''
|
||||
};
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
function downloadFile(content, filename, mimeType = 'text/plain') {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 显示状态信息
|
||||
function showStatus(element, message, type = 'info') {
|
||||
if (!element) return;
|
||||
|
||||
element.classList.remove('hidden');
|
||||
element.textContent = message;
|
||||
|
||||
// 移除所有状态类
|
||||
element.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
||||
element.classList.remove('border-green-500', 'bg-green-50', 'text-green-700');
|
||||
element.classList.remove('border-red-500', 'bg-red-50', 'text-red-700');
|
||||
element.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
||||
|
||||
// 根据类型添加相应的样式类
|
||||
switch (type) {
|
||||
case 'success':
|
||||
element.classList.add('border-green-500', 'bg-green-50', 'text-green-700');
|
||||
break;
|
||||
case 'error':
|
||||
element.classList.add('border-red-500', 'bg-red-50', 'text-red-700');
|
||||
break;
|
||||
case 'loading':
|
||||
element.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
||||
break;
|
||||
default:
|
||||
element.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
||||
}
|
||||
}
|
||||
|
||||
// 本地存储操作
|
||||
const storage = {
|
||||
// 保存数据到本地存储
|
||||
set(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('保存到本地存储失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 从本地存储获取数据
|
||||
get(key, defaultValue = null) {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (error) {
|
||||
console.error('从本地存储获取数据失败:', error);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除本地存储中的数据
|
||||
remove(key) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('删除本地存储数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖函数
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(date = new Date()) {
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 深拷贝对象
|
||||
function deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return obj;
|
||||
if (obj instanceof Date) return new Date(obj.getTime());
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item));
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查对象是否为空
|
||||
function isEmpty(obj) {
|
||||
if (obj == null) return true;
|
||||
if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
|
||||
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 自动调整文本域高度
|
||||
function autoResizeTextarea(textarea) {
|
||||
if (!textarea) return;
|
||||
|
||||
// 重置高度以获取正确的scrollHeight
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
// 计算新高度,限制最大高度
|
||||
const newHeight = Math.min(textarea.scrollHeight, 120); // 最大120px(约5行)
|
||||
textarea.style.height = newHeight + 'px';
|
||||
}
|
||||
|
||||
// 流式文本处理
|
||||
class StreamProcessor {
|
||||
constructor(onChunk, onComplete) {
|
||||
this.onChunk = onChunk;
|
||||
this.onComplete = onComplete;
|
||||
this.buffer = '';
|
||||
}
|
||||
|
||||
// 处理数据块
|
||||
processChunk(chunk) {
|
||||
this.buffer += chunk;
|
||||
|
||||
// 尝试解析完整的JSON行
|
||||
const lines = this.buffer.split('\n');
|
||||
this.buffer = lines.pop() || ''; // 保留不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
// 处理SSE格式
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
if (data === '[DONE]') {
|
||||
this.onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
this.onChunk(parsed);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('解析流数据失败:', error, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建流式请求
|
||||
async function createStreamRequest(url, options, onChunk, onComplete) {
|
||||
const processor = new StreamProcessor(onChunk, onComplete);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
processor.processChunk(chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出工具函数
|
||||
window.Utils = {
|
||||
escapeHtml,
|
||||
scrollToBottom,
|
||||
generateId,
|
||||
parseSVGResponse,
|
||||
downloadFile,
|
||||
showStatus,
|
||||
storage,
|
||||
debounce,
|
||||
throttle,
|
||||
formatDateTime,
|
||||
deepClone,
|
||||
isEmpty,
|
||||
autoResizeTextarea,
|
||||
StreamProcessor,
|
||||
createStreamRequest
|
||||
};
|
||||
/**
|
||||
* 工具函数集合
|
||||
*/
|
||||
|
||||
// HTML转义,防止XSS攻击
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 滚动到指定元素的底部
|
||||
function scrollToBottom(element) {
|
||||
if (element) {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
function generateId(prefix = 'id') {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
// 解析SVG响应,提取SVG内容和前后文本,容错缺失的结束反引号
|
||||
function parseSVGResponse(response = '') {
|
||||
const content = typeof response === 'string' ? response : String(response || '');
|
||||
const svgFenceRegex = /```(?:svg)?\s*([\s\S]*?)```/i;
|
||||
const fenceMatch = content.match(svgFenceRegex);
|
||||
|
||||
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: 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
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
svgContent: null,
|
||||
beforeText: content.trim(),
|
||||
afterText: ''
|
||||
};
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
function downloadFile(content, filename, mimeType = 'text/plain') {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 显示状态信息
|
||||
function showStatus(element, message, type = 'info') {
|
||||
if (!element) return;
|
||||
|
||||
element.classList.remove('hidden');
|
||||
element.textContent = message;
|
||||
|
||||
// 移除所有状态类
|
||||
element.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
||||
element.classList.remove('border-green-500', 'bg-green-50', 'text-green-700');
|
||||
element.classList.remove('border-red-500', 'bg-red-50', 'text-red-700');
|
||||
element.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
||||
|
||||
// 根据类型添加相应的样式类
|
||||
switch (type) {
|
||||
case 'success':
|
||||
element.classList.add('border-green-500', 'bg-green-50', 'text-green-700');
|
||||
break;
|
||||
case 'error':
|
||||
element.classList.add('border-red-500', 'bg-red-50', 'text-red-700');
|
||||
break;
|
||||
case 'loading':
|
||||
element.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
||||
break;
|
||||
default:
|
||||
element.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
||||
}
|
||||
}
|
||||
|
||||
// 本地存储操作
|
||||
const storage = {
|
||||
// 保存数据到本地存储
|
||||
set(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('保存到本地存储失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 从本地存储获取数据
|
||||
get(key, defaultValue = null) {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (error) {
|
||||
console.error('从本地存储获取数据失败:', error);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
// 删除本地存储中的数据
|
||||
remove(key) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('删除本地存储数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖函数
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
function throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(date = new Date()) {
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// 深拷贝对象
|
||||
function deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return obj;
|
||||
if (obj instanceof Date) return new Date(obj.getTime());
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item));
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查对象是否为空
|
||||
function isEmpty(obj) {
|
||||
if (obj == null) return true;
|
||||
if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
|
||||
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 自动调整文本域高度
|
||||
function autoResizeTextarea(textarea) {
|
||||
if (!textarea) return;
|
||||
|
||||
// 重置高度以获取正确的scrollHeight
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
// 计算新高度,限制最大高度
|
||||
const newHeight = Math.min(textarea.scrollHeight, 120); // 最大120px(约5行)
|
||||
textarea.style.height = newHeight + 'px';
|
||||
}
|
||||
|
||||
// 流式文本处理
|
||||
class StreamProcessor {
|
||||
constructor(onChunk, onComplete) {
|
||||
this.onChunk = onChunk;
|
||||
this.onComplete = onComplete;
|
||||
this.buffer = '';
|
||||
this.completed = false;
|
||||
}
|
||||
|
||||
complete(info = {}) {
|
||||
if (this.completed) return;
|
||||
this.completed = true;
|
||||
if (typeof this.onComplete === 'function') {
|
||||
this.onComplete(info);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理数据块
|
||||
processChunk(chunk) {
|
||||
this.buffer += chunk;
|
||||
|
||||
// 尝试解析完整的JSON行
|
||||
const lines = this.buffer.split('\n');
|
||||
this.buffer = lines.pop() || ''; // 保留不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
// 处理SSE格式
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
if (data === '[DONE]') {
|
||||
this.complete({ aborted: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
this.onChunk(parsed);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('解析流数据失败:', error, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建流式请求
|
||||
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',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
processor.processChunk(chunk);
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// 导出工具函数
|
||||
window.Utils = {
|
||||
escapeHtml,
|
||||
scrollToBottom,
|
||||
generateId,
|
||||
parseSVGResponse,
|
||||
downloadFile,
|
||||
showStatus,
|
||||
storage,
|
||||
debounce,
|
||||
throttle,
|
||||
formatDateTime,
|
||||
deepClone,
|
||||
isEmpty,
|
||||
autoResizeTextarea,
|
||||
StreamProcessor,
|
||||
createStreamRequest
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user