- 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 改用“处理中”状态并重绘气泡(按最新可见状态),避免历史重复
生成。
This commit is contained in:
696
css/style.css
696
css/style.css
@@ -1,351 +1,347 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 狂野线条效果 */
|
/* 狂野线条效果 */
|
||||||
.wild-border {
|
.wild-border {
|
||||||
border: 3px solid;
|
border: 3px solid;
|
||||||
box-shadow: 4px 4px 0px rgba(0,0,0,0.3);
|
box-shadow: 4px 4px 0px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 切换按钮激活状态 */
|
/* 切换按钮激活状态 */
|
||||||
.mode-btn-active {
|
.mode-btn-active {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 0 rgba(0,0,0,0.3);
|
box-shadow: 0 4px 0 rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-btn-inactive {
|
.mode-btn-inactive {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 对话气泡样式 */
|
/* 对话气泡样式 */
|
||||||
.chat-bubble-user {
|
.chat-bubble-user {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
box-shadow: 2px 2px 0 rgba(0,0,0,0.2);
|
box-shadow: 2px 2px 0 rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai {
|
.chat-bubble-ai {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
max-width: 85%;
|
max-width: 85%;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 2px solid #10b981;
|
border: 2px solid #10b981;
|
||||||
box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3);
|
box-shadow: 2px 2px 0 rgba(16, 185, 129, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown样式 */
|
/* Markdown样式 */
|
||||||
.chat-bubble-ai h1 {
|
.chat-bubble-ai h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai h2 {
|
.chat-bubble-ai h2 {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai h3 {
|
.chat-bubble-ai h3 {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai p {
|
.chat-bubble-ai p {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai ul, .chat-bubble-ai ol {
|
.chat-bubble-ai ul, .chat-bubble-ai ol {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai li {
|
.chat-bubble-ai li {
|
||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai code {
|
.chat-bubble-ai code {
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #e11d48;
|
color: #e11d48;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai pre {
|
.chat-bubble-ai pre {
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai pre code {
|
.chat-bubble-ai pre code {
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai blockquote {
|
.chat-bubble-ai blockquote {
|
||||||
border-left: 4px solid #d1d5db;
|
border-left: 4px solid #d1d5db;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai strong {
|
.chat-bubble-ai strong {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai em {
|
.chat-bubble-ai em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai a {
|
.chat-bubble-ai a {
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai a:hover {
|
.chat-bubble-ai a:hover {
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai table {
|
.chat-bubble-ai table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai th, .chat-bubble-ai td {
|
.chat-bubble-ai th, .chat-bubble-ai td {
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-ai th {
|
.chat-bubble-ai th {
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SVG占位符样式 - 块级换行 + 新配色 */
|
/* SVG占位符样式 - 块级换行 + 新配色 */
|
||||||
.svg-placeholder-block {
|
.svg-placeholder-block {
|
||||||
display: block;
|
display: block;
|
||||||
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
|
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-placeholder-block:hover {
|
.svg-placeholder-block:hover {
|
||||||
transform: translateX(2px) translateY(-2px);
|
transform: translateX(2px) translateY(-2px);
|
||||||
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
||||||
background: linear-gradient(135deg, #fb923c 0%, #f87171 100%);
|
background: linear-gradient(135deg, #fb923c 0%, #f87171 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SVG绘制中状态占位符 */
|
/* SVG绘制中状态占位符 */
|
||||||
.svg-drawing-placeholder {
|
.svg-drawing-placeholder {
|
||||||
display: block;
|
display: block;
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
box-shadow: 3px 3px 0 rgba(0,0,0,0.25);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-drawing-placeholder:hover {
|
.svg-drawing-placeholder:hover {
|
||||||
transform: translateX(2px) translateY(-2px);
|
transform: translateX(2px) translateY(-2px);
|
||||||
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
box-shadow: 4px 4px 0 rgba(0,0,0,0.3);
|
||||||
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
|
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 绘制中动画效果 */
|
/* 绘制中动画效果 */
|
||||||
@keyframes drawing-pulse {
|
@keyframes drawing-pulse {
|
||||||
0% { opacity: 1; }
|
0% { opacity: 1; }
|
||||||
50% { opacity: 0.7; }
|
50% { opacity: 0.7; }
|
||||||
100% { opacity: 1; }
|
100% { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-drawing-placeholder::before {
|
.svg-drawing-placeholder::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||||
animation: drawing-shine 2s infinite;
|
animation: drawing-shine 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes drawing-shine {
|
@keyframes drawing-shine {
|
||||||
0% { left: -100%; }
|
0% { left: -100%; }
|
||||||
100% { left: 100%; }
|
100% { left: 100%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-drawing-text {
|
.svg-drawing-text {
|
||||||
animation: drawing-pulse 1.5s infinite;
|
animation: drawing-pulse 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 气泡操作按钮 */
|
/* 气泡操作按钮 */
|
||||||
.bubble-action-btn {
|
.bubble-action-btn {
|
||||||
opacity: 0;
|
opacity: 1;
|
||||||
transition: opacity 0.2s;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);}
|
||||||
@keyframes wave {
|
}
|
||||||
0%, 100% {transform: translateX(0px) rotate(90deg);}
|
|
||||||
10%, 30%, 50%, 70%, 90% {transform: translateX(-1px) rotate(90deg);}
|
.wave-hand {
|
||||||
20%, 40%, 60%, 80% {transform: translateX(1px) rotate(90deg);}
|
animation: wave 3s ease-in-out infinite;
|
||||||
}
|
display: inline-block;
|
||||||
|
transform: 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;
|
||||||
.modal-overlay {
|
left: 0;
|
||||||
display: none;
|
right: 0;
|
||||||
position: fixed;
|
bottom: 0;
|
||||||
top: 0;
|
background: rgba(0, 0, 0, 0.7);
|
||||||
left: 0;
|
z-index: 1000;
|
||||||
right: 0;
|
align-items: center;
|
||||||
bottom: 0;
|
justify-content: center;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
}
|
||||||
z-index: 1000;
|
|
||||||
align-items: center;
|
.modal-overlay.active {
|
||||||
justify-content: center;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-overlay.active {
|
.modal-content {
|
||||||
display: flex;
|
background: white;
|
||||||
}
|
border: 4px solid #000;
|
||||||
|
box-shadow: 8px 8px 0 rgba(0,0,0,0.4);
|
||||||
.modal-content {
|
max-width: 500px;
|
||||||
background: white;
|
width: 90%;
|
||||||
border: 4px solid #000;
|
max-height: 90vh;
|
||||||
box-shadow: 8px 8px 0 rgba(0,0,0,0.4);
|
overflow-y: auto;
|
||||||
max-width: 500px;
|
}
|
||||||
width: 90%;
|
|
||||||
max-height: 90vh;
|
/* 表单输入框样式 */
|
||||||
overflow-y: auto;
|
.config-input {
|
||||||
}
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
/* 表单输入框样式 */
|
border: 2px solid #000;
|
||||||
.config-input {
|
font-size: 14px;
|
||||||
width: 100%;
|
transition: all 0.2s;
|
||||||
padding: 10px;
|
}
|
||||||
border: 2px solid #000;
|
|
||||||
font-size: 14px;
|
.config-input:focus {
|
||||||
transition: all 0.2s;
|
outline: none;
|
||||||
}
|
border-color: #667eea;
|
||||||
|
box-shadow: 3px 3px 0 rgba(102, 126, 234, 0.3);
|
||||||
.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); }
|
||||||
/* 齿轮旋转动画 */
|
}
|
||||||
@keyframes rotate {
|
|
||||||
from { transform: rotate(0deg); }
|
.settings-btn:hover iconify-icon {
|
||||||
to { transform: rotate(360deg); }
|
animation: rotate 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-btn:hover iconify-icon {
|
/* 自适应高度输入框 */
|
||||||
animation: rotate 1s linear infinite;
|
.auto-resize-input {
|
||||||
}
|
min-height: 40px;
|
||||||
|
max-height: 120px; /* 5行左右的高度 */
|
||||||
/* 自适应高度输入框 */
|
resize: none;
|
||||||
.auto-resize-input {
|
overflow-y: auto;
|
||||||
min-height: 40px;
|
line-height: 1.5;
|
||||||
max-height: 120px; /* 5行左右的高度 */
|
padding: 8px 12px;
|
||||||
resize: none;
|
}
|
||||||
overflow-y: auto;
|
|
||||||
line-height: 1.5;
|
/* 清空按钮样式 */
|
||||||
padding: 8px 12px;
|
.clear-btn {
|
||||||
}
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
/* 清空按钮样式 */
|
|
||||||
.clear-btn {
|
.clear-btn:hover {
|
||||||
transition: all 0.2s;
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-btn:hover {
|
.clear-btn:active {
|
||||||
transform: scale(1.05);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-btn:active {
|
/* 流式输出动画 */
|
||||||
transform: scale(0.95);
|
@keyframes fadeIn {
|
||||||
}
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
/* 流式输出动画 */
|
}
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
.streaming-text {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.streaming-text {
|
/* 打字机效果光标 */
|
||||||
animation: fadeIn 0.3s ease-in-out;
|
@keyframes blink {
|
||||||
}
|
0%, 50% { opacity: 1; }
|
||||||
|
51%, 100% { opacity: 0; }
|
||||||
/* 打字机效果光标 */
|
}
|
||||||
@keyframes blink {
|
|
||||||
0%, 50% { opacity: 1; }
|
.typing-cursor::after {
|
||||||
51%, 100% { opacity: 0; }
|
content: '|';
|
||||||
}
|
animation: blink 1s infinite;
|
||||||
|
color: #667eea;
|
||||||
.typing-cursor::after {
|
font-weight: bold;
|
||||||
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); }
|
||||||
@keyframes shake {
|
}
|
||||||
0%, 100% { transform: translateX(0); }
|
|
||||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
|
.clear-history-btn:hover iconify-icon {
|
||||||
20%, 40%, 60%, 80% { transform: translateX(2px); }
|
animation: shake 0.5s ease-in-out;
|
||||||
}
|
|
||||||
|
|
||||||
.clear-history-btn:hover iconify-icon {
|
|
||||||
animation: shake 0.5s ease-in-out;
|
|
||||||
}
|
}
|
||||||
596
js/apiclient.js
596
js/apiclient.js
@@ -1,299 +1,299 @@
|
|||||||
/**
|
/**
|
||||||
* API客户端 - 处理与AI服务的交互
|
* API客户端 - 处理与AI服务的交互
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = {
|
this.config = {
|
||||||
url: '',
|
url: '',
|
||||||
key: '',
|
key: '',
|
||||||
model: ''
|
model: ''
|
||||||
};
|
};
|
||||||
this.prompts = {
|
this.prompts = {
|
||||||
canvas: '',
|
canvas: '',
|
||||||
swot: ''
|
swot: ''
|
||||||
};
|
};
|
||||||
this.loadConfig();
|
this.loadConfig();
|
||||||
this.loadPrompts();
|
this.loadPrompts();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载API配置
|
// 加载API配置
|
||||||
loadConfig() {
|
loadConfig() {
|
||||||
const savedConfig = Utils.storage.get('apiConfig');
|
const savedConfig = Utils.storage.get('apiConfig');
|
||||||
if (savedConfig) {
|
if (savedConfig) {
|
||||||
this.config = { ...this.config, ...savedConfig };
|
this.config = { ...this.config, ...savedConfig };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载系统提示词
|
// 加载系统提示词
|
||||||
async loadPrompts() {
|
async loadPrompts() {
|
||||||
try {
|
try {
|
||||||
// 加载产品画布提示词
|
// 加载产品画布提示词
|
||||||
const canvasResponse = await fetch('prompts/canvas-prompt.txt');
|
const canvasResponse = await fetch('prompts/canvas-prompt.txt');
|
||||||
this.prompts.canvas = await canvasResponse.text();
|
this.prompts.canvas = await canvasResponse.text();
|
||||||
|
|
||||||
// 加载SWOT分析提示词
|
// 加载SWOT分析提示词
|
||||||
const swotResponse = await fetch('prompts/swot-prompt.txt');
|
const swotResponse = await fetch('prompts/swot-prompt.txt');
|
||||||
this.prompts.swot = await swotResponse.text();
|
this.prompts.swot = await swotResponse.text();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载提示词失败:', error);
|
console.error('加载提示词失败:', error);
|
||||||
// 使用默认提示词
|
// 使用默认提示词
|
||||||
this.prompts.canvas = '你是一个专业的产品战略分析师,擅长创建产品画布。';
|
this.prompts.canvas = '你是一个专业的产品战略分析师,擅长创建产品画布。';
|
||||||
this.prompts.swot = '你是一个专业的商业战略分析师,擅长进行SWOT分析。';
|
this.prompts.swot = '你是一个专业的商业战略分析师,擅长进行SWOT分析。';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存API配置
|
// 保存API配置
|
||||||
saveConfig(config) {
|
saveConfig(config) {
|
||||||
this.config = { ...this.config, ...config };
|
this.config = { ...this.config, ...config };
|
||||||
return Utils.storage.set('apiConfig', this.config);
|
return Utils.storage.set('apiConfig', this.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前配置
|
// 获取当前配置
|
||||||
getConfig() {
|
getConfig() {
|
||||||
return { ...this.config };
|
return { ...this.config };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证配置是否完整
|
// 验证配置是否完整
|
||||||
isConfigValid() {
|
isConfigValid() {
|
||||||
return this.config.url && this.config.key && this.config.model;
|
return this.config.url && this.config.key && this.config.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试API连接
|
// 测试API连接
|
||||||
async testConnection() {
|
async testConnection() {
|
||||||
if (!this.isConfigValid()) {
|
if (!this.isConfigValid()) {
|
||||||
throw new Error('API配置不完整,请填写所有字段');
|
throw new Error('API配置不完整,请填写所有字段');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.makeRequest([
|
const response = await this.makeRequest([
|
||||||
{ role: 'user', content: '测试连接' }
|
{ role: 'user', content: '测试连接' }
|
||||||
], 5);
|
], 5);
|
||||||
|
|
||||||
return { success: true, data: response };
|
return { success: true, data: response };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`连接测试失败: ${error.message}`);
|
throw new Error(`连接测试失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送聊天请求
|
// 发送聊天请求
|
||||||
async sendChatMessage(messages, options = {}) {
|
async sendChatMessage(messages, options = {}) {
|
||||||
if (!this.isConfigValid()) {
|
if (!this.isConfigValid()) {
|
||||||
throw new Error('API配置不完整,请先配置API设置');
|
throw new Error('API配置不完整,请先配置API设置');
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxTokens = options.maxTokens || 2000;
|
const maxTokens = options.maxTokens || 2000;
|
||||||
const temperature = options.temperature || 0.7;
|
const temperature = options.temperature || 0.7;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.makeRequest(messages, maxTokens, temperature);
|
const response = await this.makeRequest(messages, maxTokens, temperature);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`API请求失败: ${error.message}`);
|
throw new Error(`API请求失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 核心请求方法
|
// 核心请求方法
|
||||||
async makeRequest(messages, maxTokens, temperature = 0.7) {
|
async makeRequest(messages, maxTokens, temperature = 0.7) {
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: this.config.model,
|
model: this.config.model,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
max_tokens: maxTokens,
|
max_tokens: maxTokens,
|
||||||
temperature: temperature
|
temperature: temperature
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(this.config.url, {
|
const response = await fetch(this.config.url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.config.key}`,
|
'Authorization': `Bearer ${this.config.key}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody)
|
body: JSON.stringify(requestBody)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!data.choices || !data.choices.length || !data.choices[0].message) {
|
if (!data.choices || !data.choices.length || !data.choices[0].message) {
|
||||||
throw new Error('API返回数据格式异常');
|
throw new Error('API返回数据格式异常');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.choices[0].message.content;
|
return data.choices[0].message.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成产品画布的专用方法
|
// 生成产品画布的专用方法
|
||||||
async generateProductCanvas(userRequest, context = []) {
|
async generateProductCanvas(userRequest, context = []) {
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: this.prompts.canvas },
|
{ role: 'system', content: this.prompts.canvas },
|
||||||
...context,
|
...context,
|
||||||
{ role: 'user', content: userRequest }
|
{ role: 'user', content: userRequest }
|
||||||
];
|
];
|
||||||
|
|
||||||
return await this.sendChatMessage(messages, { maxTokens: 18000 });
|
return await this.sendChatMessage(messages, { maxTokens: 18000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成SWOT分析的专用方法
|
// 生成SWOT分析的专用方法
|
||||||
async generateSWOTAnalysis(userRequest, context = []) {
|
async generateSWOTAnalysis(userRequest, context = []) {
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: this.prompts.swot },
|
{ role: 'system', content: this.prompts.swot },
|
||||||
...context,
|
...context,
|
||||||
{ role: 'user', content: userRequest }
|
{ role: 'user', content: userRequest }
|
||||||
];
|
];
|
||||||
|
|
||||||
return await this.sendChatMessage(messages, { maxTokens: 18000 });
|
return await this.sendChatMessage(messages, { maxTokens: 18000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流式生成产品画布
|
// 流式生成产品画布
|
||||||
async generateProductCanvasStream(userRequest, context = [], onChunk, onComplete) {
|
async generateProductCanvasStream(userRequest, context = [], onChunk, onComplete) {
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: this.prompts.canvas },
|
{ role: 'system', content: this.prompts.canvas },
|
||||||
...context,
|
...context,
|
||||||
{ role: 'user', content: userRequest }
|
{ role: 'user', content: userRequest }
|
||||||
];
|
];
|
||||||
|
|
||||||
return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
|
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流式生成SWOT分析
|
// 流式生成SWOT分析
|
||||||
async generateSWOTAnalysisStream(userRequest, context = [], onChunk, onComplete) {
|
async generateSWOTAnalysisStream(userRequest, context = [], onChunk, onComplete) {
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: this.prompts.swot },
|
{ role: 'system', content: this.prompts.swot },
|
||||||
...context,
|
...context,
|
||||||
{ role: 'user', content: userRequest }
|
{ role: 'user', content: userRequest }
|
||||||
];
|
];
|
||||||
|
|
||||||
return await this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
|
return this.sendChatMessageStream(messages, { maxTokens: 13000 }, onChunk, onComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流式发送聊天请求
|
// 流式发送聊天请求
|
||||||
async sendChatMessageStream(messages, options = {}, onChunk, onComplete) {
|
async sendChatMessageStream(messages, options = {}, onChunk, onComplete) {
|
||||||
if (!this.isConfigValid()) {
|
if (!this.isConfigValid()) {
|
||||||
throw new Error('API配置不完整,请先配置API设置');
|
throw new Error('API配置不完整,请先配置API设置');
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxTokens = options.maxTokens || 2000;
|
const maxTokens = options.maxTokens || 2000;
|
||||||
const temperature = options.temperature || 0.7;
|
const temperature = options.temperature || 0.7;
|
||||||
const stream = true;
|
const stream = true;
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: this.config.model,
|
model: this.config.model,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
max_tokens: maxTokens,
|
max_tokens: maxTokens,
|
||||||
temperature: temperature,
|
temperature: temperature,
|
||||||
stream: stream
|
stream: stream
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = this.config.url.replace('/chat/completions', '/chat/completions');
|
const url = this.config.url.replace('/chat/completions', '/chat/completions');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Utils.createStreamRequest(
|
return Utils.createStreamRequest(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.config.key}`,
|
'Authorization': `Bearer ${this.config.key}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody)
|
body: JSON.stringify(requestBody)
|
||||||
},
|
},
|
||||||
onChunk,
|
onChunk,
|
||||||
onComplete
|
onComplete
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`流式API请求失败: ${error.message}`);
|
throw new Error(`流式API请求失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新生成响应
|
// 重新生成响应
|
||||||
async regenerateResponse(messageId, conversationHistory) {
|
async regenerateResponse(messageId, conversationHistory) {
|
||||||
// 找到指定消息ID之前的所有对话历史
|
// 找到指定消息ID之前的所有对话历史
|
||||||
const contextMessages = conversationHistory
|
const contextMessages = conversationHistory
|
||||||
.filter(msg => msg.id <= messageId)
|
.filter(msg => msg.id <= messageId)
|
||||||
.map(msg => ({
|
.map(msg => ({
|
||||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
content: msg.content
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (contextMessages.length === 0) {
|
if (contextMessages.length === 0) {
|
||||||
throw new Error('没有找到有效的对话上下文');
|
throw new Error('没有找到有效的对话上下文');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除最后一条消息(需要重新生成的消息)
|
// 移除最后一条消息(需要重新生成的消息)
|
||||||
if (contextMessages.length > 0 && contextMessages[contextMessages.length - 1].role === 'assistant') {
|
if (contextMessages.length > 0 && contextMessages[contextMessages.length - 1].role === 'assistant') {
|
||||||
contextMessages.pop();
|
contextMessages.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据当前模式选择相应的生成方法
|
// 根据当前模式选择相应的生成方法
|
||||||
const lastUserMessage = contextMessages.filter(msg => msg.role === 'user').pop();
|
const lastUserMessage = contextMessages.filter(msg => msg.role === 'user').pop();
|
||||||
if (!lastUserMessage) {
|
if (!lastUserMessage) {
|
||||||
throw new Error('没有找到用户消息');
|
throw new Error('没有找到用户消息');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode = Utils.storage.get('currentMode', 'canvas');
|
const mode = Utils.storage.get('currentMode', 'canvas');
|
||||||
|
|
||||||
if (mode === 'canvas') {
|
if (mode === 'canvas') {
|
||||||
return await this.generateProductCanvas(lastUserMessage.content, contextMessages.slice(0, -1));
|
return await this.generateProductCanvas(lastUserMessage.content, contextMessages.slice(0, -1));
|
||||||
} else {
|
} else {
|
||||||
return await this.generateSWOTAnalysis(lastUserMessage.content, contextMessages.slice(0, -1));
|
return await this.generateSWOTAnalysis(lastUserMessage.content, contextMessages.slice(0, -1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟API响应(用于测试)
|
// 模拟API响应(用于测试)
|
||||||
simulateAPIResponse(userMessage, mode = 'canvas') {
|
simulateAPIResponse(userMessage, mode = 'canvas') {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const mockResponses = [
|
const mockResponses = [
|
||||||
`好的!我为您生成了一个${mode === 'canvas' ? '产品画布' : 'SWOT分析'}
|
`好的!我为您生成了一个${mode === 'canvas' ? '产品画布' : 'SWOT分析'}
|
||||||
\`\`\`svg
|
\`\`\`svg
|
||||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
||||||
<rect x="50" y="50" width="500" height="300" fill="url(#grad1)" rx="10"/>
|
<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">
|
<text x="300" y="200" text-anchor="middle" font-size="24" fill="white" font-weight="bold">
|
||||||
这是${mode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG
|
这是${mode === 'canvas' ? '产品画布' : 'SWOT分析'}示例SVG
|
||||||
</text>
|
</text>
|
||||||
<circle cx="150" cy="150" r="40" fill="#ffffff" opacity="0.3"/>
|
<circle cx="150" cy="150" r="40" fill="#ffffff" opacity="0.3"/>
|
||||||
<circle cx="450" cy="250" r="30" fill="#ffffff" opacity="0.3"/>
|
<circle cx="450" cy="250" r="30" fill="#ffffff" opacity="0.3"/>
|
||||||
</svg>
|
</svg>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
|
包含了关键要素和模块。点击上方标签可在右侧查看详细图表。`,
|
||||||
|
|
||||||
`已经为您调整完成!
|
`已经为您调整完成!
|
||||||
\`\`\`svg
|
\`\`\`svg
|
||||||
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
|
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
|
||||||
<stop offset="100%" style="stop-color:#ef4444;stop-opacity:1" />
|
<stop offset="100%" style="stop-color:#ef4444;stop-opacity:1" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="#fff"/>
|
<rect width="100%" height="100%" fill="#fff"/>
|
||||||
<rect x="75" y="75" width="450" height="250" fill="url(#grad2)" rx="15"/>
|
<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">
|
<text x="300" y="200" text-anchor="middle" font-size="28" fill="white" font-weight="bold">
|
||||||
${mode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'}
|
${mode === 'canvas' ? '优化后的产品画布' : '优化后的SWOT分析'}
|
||||||
</text>
|
</text>
|
||||||
<rect x="100" y="120" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
|
<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"/>
|
<rect x="420" y="220" width="80" height="60" fill="#ffffff" opacity="0.2" rx="5"/>
|
||||||
</svg>
|
</svg>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
采用了更加鲜明的色彩组合,希望您满意!`
|
采用了更加鲜明的色彩组合,希望您满意!`
|
||||||
];
|
];
|
||||||
|
|
||||||
const response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
|
const response = mockResponses[Math.floor(Math.random() * mockResponses.length)];
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}, 1000 + Math.random() * 1000); // 1-2秒的随机延迟
|
}, 1000 + Math.random() * 1000); // 1-2秒的随机延迟
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建全局API客户端实例
|
// 创建全局API客户端实例
|
||||||
window.apiClient = new APIClient();
|
window.apiClient = new APIClient();
|
||||||
559
js/utils.js
559
js/utils.js
@@ -1,26 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* 工具函数集合
|
* 工具函数集合
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// HTML转义,防止XSS攻击
|
// HTML转义,防止XSS攻击
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到指定元素的底部
|
// 滚动到指定元素的底部
|
||||||
function scrollToBottom(element) {
|
function scrollToBottom(element) {
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollTop = element.scrollHeight;
|
element.scrollTop = element.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
function generateId(prefix = 'id') {
|
function generateId(prefix = 'id') {
|
||||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析SVG响应,提取SVG内容和前后文本,容错缺失的结束反引号
|
// 解析SVG响应,提取SVG内容和前后文本,容错缺失的结束反引号
|
||||||
function parseSVGResponse(response = '') {
|
function parseSVGResponse(response = '') {
|
||||||
const content = typeof response === 'string' ? response : String(response || '');
|
const content = typeof response === 'string' ? response : String(response || '');
|
||||||
@@ -78,246 +78,275 @@ function parseSVGResponse(response = '') {
|
|||||||
afterText: ''
|
afterText: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载文件
|
// 下载文件
|
||||||
function downloadFile(content, filename, mimeType = 'text/plain') {
|
function downloadFile(content, filename, mimeType = 'text/plain') {
|
||||||
const blob = new Blob([content], { type: mimeType });
|
const blob = new Blob([content], { type: mimeType });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示状态信息
|
// 显示状态信息
|
||||||
function showStatus(element, message, type = 'info') {
|
function showStatus(element, message, type = 'info') {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
element.classList.remove('hidden');
|
element.classList.remove('hidden');
|
||||||
element.textContent = message;
|
element.textContent = message;
|
||||||
|
|
||||||
// 移除所有状态类
|
// 移除所有状态类
|
||||||
element.classList.remove('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
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-green-500', 'bg-green-50', 'text-green-700');
|
||||||
element.classList.remove('border-red-500', 'bg-red-50', 'text-red-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');
|
element.classList.remove('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
||||||
|
|
||||||
// 根据类型添加相应的样式类
|
// 根据类型添加相应的样式类
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success':
|
case 'success':
|
||||||
element.classList.add('border-green-500', 'bg-green-50', 'text-green-700');
|
element.classList.add('border-green-500', 'bg-green-50', 'text-green-700');
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
element.classList.add('border-red-500', 'bg-red-50', 'text-red-700');
|
element.classList.add('border-red-500', 'bg-red-50', 'text-red-700');
|
||||||
break;
|
break;
|
||||||
case 'loading':
|
case 'loading':
|
||||||
element.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
element.classList.add('border-blue-500', 'bg-blue-50', 'text-blue-700');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
element.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
element.classList.add('border-gray-300', 'bg-gray-50', 'text-gray-600');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地存储操作
|
// 本地存储操作
|
||||||
const storage = {
|
const storage = {
|
||||||
// 保存数据到本地存储
|
// 保存数据到本地存储
|
||||||
set(key, value) {
|
set(key, value) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存到本地存储失败:', error);
|
console.error('保存到本地存储失败:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 从本地存储获取数据
|
// 从本地存储获取数据
|
||||||
get(key, defaultValue = null) {
|
get(key, defaultValue = null) {
|
||||||
try {
|
try {
|
||||||
const item = localStorage.getItem(key);
|
const item = localStorage.getItem(key);
|
||||||
return item ? JSON.parse(item) : defaultValue;
|
return item ? JSON.parse(item) : defaultValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('从本地存储获取数据失败:', error);
|
console.error('从本地存储获取数据失败:', error);
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除本地存储中的数据
|
// 删除本地存储中的数据
|
||||||
remove(key) {
|
remove(key) {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除本地存储数据失败:', error);
|
console.error('删除本地存储数据失败:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 防抖函数
|
// 防抖函数
|
||||||
function debounce(func, wait) {
|
function debounce(func, wait) {
|
||||||
let timeout;
|
let timeout;
|
||||||
return function executedFunction(...args) {
|
return function executedFunction(...args) {
|
||||||
const later = () => {
|
const later = () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
func(...args);
|
func(...args);
|
||||||
};
|
};
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(later, wait);
|
timeout = setTimeout(later, wait);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 节流函数
|
// 节流函数
|
||||||
function throttle(func, limit) {
|
function throttle(func, limit) {
|
||||||
let inThrottle;
|
let inThrottle;
|
||||||
return function() {
|
return function() {
|
||||||
const args = arguments;
|
const args = arguments;
|
||||||
const context = this;
|
const context = this;
|
||||||
if (!inThrottle) {
|
if (!inThrottle) {
|
||||||
func.apply(context, args);
|
func.apply(context, args);
|
||||||
inThrottle = true;
|
inThrottle = true;
|
||||||
setTimeout(() => inThrottle = false, limit);
|
setTimeout(() => inThrottle = false, limit);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化日期时间
|
// 格式化日期时间
|
||||||
function formatDateTime(date = new Date()) {
|
function formatDateTime(date = new Date()) {
|
||||||
return date.toLocaleString('zh-CN', {
|
return date.toLocaleString('zh-CN', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit'
|
second: '2-digit'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 深拷贝对象
|
// 深拷贝对象
|
||||||
function deepClone(obj) {
|
function deepClone(obj) {
|
||||||
if (obj === null || typeof obj !== 'object') return obj;
|
if (obj === null || typeof obj !== 'object') return obj;
|
||||||
if (obj instanceof Date) return new Date(obj.getTime());
|
if (obj instanceof Date) return new Date(obj.getTime());
|
||||||
if (obj instanceof Array) return obj.map(item => deepClone(item));
|
if (obj instanceof Array) return obj.map(item => deepClone(item));
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
const clonedObj = {};
|
const clonedObj = {};
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj.hasOwnProperty(key)) {
|
if (obj.hasOwnProperty(key)) {
|
||||||
clonedObj[key] = deepClone(obj[key]);
|
clonedObj[key] = deepClone(obj[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return clonedObj;
|
return clonedObj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查对象是否为空
|
// 检查对象是否为空
|
||||||
function isEmpty(obj) {
|
function isEmpty(obj) {
|
||||||
if (obj == null) return true;
|
if (obj == null) return true;
|
||||||
if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
|
if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
|
||||||
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
if (typeof obj === 'object') return Object.keys(obj).length === 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动调整文本域高度
|
// 自动调整文本域高度
|
||||||
function autoResizeTextarea(textarea) {
|
function autoResizeTextarea(textarea) {
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
|
|
||||||
// 重置高度以获取正确的scrollHeight
|
// 重置高度以获取正确的scrollHeight
|
||||||
textarea.style.height = 'auto';
|
textarea.style.height = 'auto';
|
||||||
|
|
||||||
// 计算新高度,限制最大高度
|
// 计算新高度,限制最大高度
|
||||||
const newHeight = Math.min(textarea.scrollHeight, 120); // 最大120px(约5行)
|
const newHeight = Math.min(textarea.scrollHeight, 120); // 最大120px(约5行)
|
||||||
textarea.style.height = newHeight + 'px';
|
textarea.style.height = newHeight + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流式文本处理
|
// 流式文本处理
|
||||||
class StreamProcessor {
|
class StreamProcessor {
|
||||||
constructor(onChunk, onComplete) {
|
constructor(onChunk, onComplete) {
|
||||||
this.onChunk = onChunk;
|
this.onChunk = onChunk;
|
||||||
this.onComplete = onComplete;
|
this.onComplete = onComplete;
|
||||||
this.buffer = '';
|
this.buffer = '';
|
||||||
}
|
this.completed = false;
|
||||||
|
}
|
||||||
// 处理数据块
|
|
||||||
processChunk(chunk) {
|
complete(info = {}) {
|
||||||
this.buffer += chunk;
|
if (this.completed) return;
|
||||||
|
this.completed = true;
|
||||||
// 尝试解析完整的JSON行
|
if (typeof this.onComplete === 'function') {
|
||||||
const lines = this.buffer.split('\n');
|
this.onComplete(info);
|
||||||
this.buffer = lines.pop() || ''; // 保留不完整的行
|
}
|
||||||
|
}
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim()) {
|
// 处理数据块
|
||||||
try {
|
processChunk(chunk) {
|
||||||
// 处理SSE格式
|
this.buffer += chunk;
|
||||||
if (line.startsWith('data: ')) {
|
|
||||||
const data = line.slice(6);
|
// 尝试解析完整的JSON行
|
||||||
if (data === '[DONE]') {
|
const lines = this.buffer.split('\n');
|
||||||
this.onComplete();
|
this.buffer = lines.pop() || ''; // 保留不完整的行
|
||||||
return;
|
|
||||||
}
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
const parsed = JSON.parse(data);
|
try {
|
||||||
this.onChunk(parsed);
|
// 处理SSE格式
|
||||||
}
|
if (line.startsWith('data: ')) {
|
||||||
} catch (error) {
|
const data = line.slice(6);
|
||||||
console.warn('解析流数据失败:', error, line);
|
if (data === '[DONE]') {
|
||||||
}
|
this.complete({ aborted: false });
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
const parsed = JSON.parse(data);
|
||||||
|
this.onChunk(parsed);
|
||||||
// 创建流式请求
|
}
|
||||||
async function createStreamRequest(url, options, onChunk, onComplete) {
|
} catch (error) {
|
||||||
const processor = new StreamProcessor(onChunk, onComplete);
|
console.warn('解析流数据失败:', error, line);
|
||||||
|
}
|
||||||
try {
|
}
|
||||||
const response = await fetch(url, {
|
}
|
||||||
...options,
|
}
|
||||||
headers: {
|
}
|
||||||
...options.headers,
|
|
||||||
'Accept': 'text/event-stream',
|
// 创建流式请求
|
||||||
'Cache-Control': 'no-cache'
|
function createStreamRequest(url, options, onChunk, onComplete) {
|
||||||
}
|
const processor = new StreamProcessor(onChunk, onComplete);
|
||||||
});
|
const controller = new AbortController();
|
||||||
|
|
||||||
if (!response.ok) {
|
const fetchPromise = (async () => {
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
try {
|
||||||
}
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
const reader = response.body.getReader();
|
signal: controller.signal,
|
||||||
const decoder = new TextDecoder();
|
headers: {
|
||||||
|
...options.headers,
|
||||||
while (true) {
|
'Accept': 'text/event-stream',
|
||||||
const { done, value } = await reader.read();
|
'Cache-Control': 'no-cache'
|
||||||
if (done) break;
|
}
|
||||||
|
});
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
|
||||||
processor.processChunk(chunk);
|
if (!response.ok) {
|
||||||
}
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
} catch (error) {
|
}
|
||||||
throw error;
|
|
||||||
}
|
const reader = response.body.getReader();
|
||||||
}
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
// 导出工具函数
|
while (true) {
|
||||||
window.Utils = {
|
const { done, value } = await reader.read();
|
||||||
escapeHtml,
|
if (done) break;
|
||||||
scrollToBottom,
|
|
||||||
generateId,
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
parseSVGResponse,
|
processor.processChunk(chunk);
|
||||||
downloadFile,
|
if (processor.completed) {
|
||||||
showStatus,
|
break;
|
||||||
storage,
|
}
|
||||||
debounce,
|
}
|
||||||
throttle,
|
|
||||||
formatDateTime,
|
if (!processor.completed) {
|
||||||
deepClone,
|
processor.complete({ aborted: false });
|
||||||
isEmpty,
|
}
|
||||||
autoResizeTextarea,
|
} catch (error) {
|
||||||
StreamProcessor,
|
if (error.name === 'AbortError') {
|
||||||
createStreamRequest
|
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
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user