<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSeek Web客户端(含LaTeX渲染)</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- KaTeX CSS for LaTeX rendering -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" crossorigin="anonymous">
<style>
/* 样式完全保持不变,与原文件相同 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
:root {
--primary: #4a90e2;
--primary-dark: #3a7bc8;
--success: #34c759;
--warning: #ff9500;
--danger: #ff3b30;
--light-bg: #f8f9fa;
--dark-bg: #1a1a1a;
--sidebar-bg: #2c3e50;
--card-bg: #ffffff;
--text: #333333;
--text-light: #666666;
--border: #e0e0e0;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--code-bg: #2c3e50;
--code-color: #ecf0f1;
--message-user: #e3f2fd;
--message-ai: #ffffff;
--message-system: #f0f7ff;
}
body {
background-color: var(--light-bg);
color: var(--text);
height: 100vh;
overflow: hidden;
}
.app-container {
display: flex;
height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 280px;
background-color: var(--sidebar-bg);
color: white;
display: flex;
flex-direction: column;
padding: 20px 0;
flex-shrink: 0;
overflow-y: auto;
}
.logo {
padding: 0 20px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
}
.logo h1 {
font-size: 22px;
font-weight: 700;
margin-bottom: 5px;
color: #fff;
}
.logo .version {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
.menu {
padding: 20px;
flex-grow: 1;
}
.menu-section {
margin-bottom: 25px;
}
.menu-section h3 {
font-size: 14px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
margin-bottom: 12px;
letter-spacing: 1px;
}
.menu-item {
display: flex;
align-items: center;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 5px;
cursor: pointer;
transition: all 0.2s;
color: rgba(255, 255, 255, 0.8);
}
.menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.menu-item.active {
background-color: var(--primary);
color: white;
}
.menu-item i {
width: 24px;
margin-right: 10px;
font-size: 16px;
}
.menu-item span {
font-size: 14px;
font-weight: 500;
}
.status-bar {
padding: 15px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
font-size: 12px;
}
.status-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
color: rgba(255, 255, 255, 0.7);
}
.status-value {
font-weight: 600;
color: white;
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
padding: 16px 24px;
background-color: var(--card-bg);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
flex-shrink: 0;
}
.chat-title {
font-size: 18px;
font-weight: 600;
color: var(--text);
}
.header-actions {
display: flex;
gap: 10px;
}
.header-btn {
padding: 8px 16px;
background-color: var(--light-bg);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: var(--text);
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.header-btn:hover {
background-color: var(--primary);
color: white;
border-color: var(--primary);
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 24px;
background-color: var(--light-bg);
background-image:
radial-gradient(circle at 25px 25px, rgba(0, 0, 0, 0.05) 2%, transparent 0%),
radial-gradient(circle at 75px 75px, rgba(0, 0, 0, 0.05) 2%, transparent 0%);
background-size: 100px 100px;
}
.message {
margin-bottom: 24px;
max-width: 85%;
animation: fadeIn 0.3s ease-out;
position: relative;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
margin-left: auto;
}
.assistant-message {
margin-right: auto;
}
.system-message {
margin-left: auto;
margin-right: auto;
max-width: 90%;
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 8px;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
font-weight: 600;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.assistant-message .message-avatar {
cursor: pointer;
}
.assistant-message .message-avatar:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.user-avatar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.assistant-avatar {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.system-avatar {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.message-sender {
font-weight: 600;
font-size: 14px;
color: var(--text);
}
.user-sender {
color: #667eea;
}
.assistant-sender {
color: #f5576c;
}
.system-sender {
color: #4facfe;
}
.message-time {
font-size: 12px;
color: var(--text-light);
margin-left: auto;
}
.message-actions {
display: flex;
gap: 6px;
margin-left: auto;
}
.message-action-btn {
background: none;
border: none;
color: var(--text-light);
cursor: pointer;
font-size: 14px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.message-action-btn:hover {
background-color: var(--light-bg);
color: var(--primary);
}
.message-content {
padding: 16px 20px;
border-radius: 12px;
line-height: 1.6;
font-size: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
white-space: pre-wrap;
overflow-wrap: break-word;
position: relative;
}
.user-content {
background: linear-gradient(135deg, var(--message-user) 0%, #d6e8ff 100%);
color: #2c3e50;
border-top-right-radius: 4px;
border-left: 4px solid #667eea;
}
.user-content:before {
content: '';
position: absolute;
top: 0;
right: -10px;
width: 0;
height: 0;
border-left: 10px solid #d6e8ff;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
}
.assistant-content {
background: linear-gradient(135deg, var(--message-ai) 0%, #f9f9f9 100%);
color: var(--text);
border-top-left-radius: 4px;
border-left: 4px solid #f5576c;
border: 1px solid #f0f0f0;
}
.assistant-content:before {
content: '';
position: absolute;
top: 0;
left: -10px;
width: 0;
height: 0;
border-right: 10px solid #f9f9f9;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
}
.assistant-content.raw-text {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
white-space: pre-wrap;
line-height: 1.5;
background-color: #f9f9f9;
padding: 12px 16px;
border-left: 4px solid var(--primary);
}
.system-content {
background: linear-gradient(135deg, var(--message-system) 0%, #e8f4ff 100%);
color: #2c3e50;
border-radius: 12px;
border-left: 4px solid #4facfe;
font-style: italic;
}
.reasoning-content {
background-color: rgba(0, 0, 0, 0.05);
color: var(--text-light);
font-style: italic;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 12px;
font-size: 14px;
border-left: 4px solid var(--warning);
white-space: pre-wrap;
background: linear-gradient(135deg, #fff8e1 0%, #fff3cd 100%);
}
.edit-textarea {
width: 100%;
padding: 12px;
border: 2px solid var(--primary);
border-radius: 8px;
font-size: 15px;
font-family: inherit;
resize: vertical;
margin-bottom: 8px;
}
.edit-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.edit-btn {
padding: 6px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.edit-save {
background-color: var(--primary);
color: white;
}
.edit-save:hover {
background-color: var(--primary-dark);
}
.edit-cancel {
background-color: var(--light-bg);
color: var(--text);
border: 1px solid var(--border);
}
.edit-cancel:hover {
background-color: #e0e0e0;
}
.empty-chat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-light);
text-align: center;
padding: 40px;
}
.empty-chat i {
font-size: 64px;
margin-bottom: 20px;
color: var(--border);
}
.empty-chat h3 {
font-size: 20px;
margin-bottom: 10px;
color: var(--text);
}
.empty-chat p {
max-width: 400px;
line-height: 1.6;
}
.input-container {
padding: 20px 24px;
background-color: var(--card-bg);
border-top: 1px solid var(--border);
flex-shrink: 0;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.input-area {
display: flex;
gap: 12px;
position: relative;
}
.message-input {
flex: 1;
padding: 14px 18px;
background-color: var(--light-bg);
border: 2px solid var(--border);
border-radius: 12px;
color: var(--text);
font-size: 15px;
resize: none;
min-height: 54px;
max-height: 200px;
line-height: 1.5;
transition: all 0.3s;
font-family: inherit;
white-space: pre-wrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.message-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1), 0 4px 12px rgba(0, 0, 0, 0.1);
}
.send-button {
padding: 0 24px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border: none;
color: white;
border-radius: 12px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
align-self: flex-end;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
min-height: 54px;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
}
.send-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08);
}
.send-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.input-actions {
display: flex;
justify-content: space-between;
margin-top: 12px;
padding: 0 8px;
}
.action-btn {
background: none;
border: none;
color: var(--text-light);
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s;
background-color: rgba(0, 0, 0, 0.02);
}
.action-btn:hover {
background-color: var(--light-bg);
color: var(--text);
transform: translateY(-1px);
}
.input-hint {
font-size: 12px;
color: var(--text-light);
margin-top: 8px;
text-align: center;
font-style: italic;
}
/* 设置面板 */
.settings-panel {
width: 350px;
background-color: var(--card-bg);
border-left: 1px solid var(--border);
padding: 0;
display: none;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
box-shadow: -2px 0 12px rgba(0, 0, 0, 0.1);
}
.settings-panel.open {
display: flex;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
.settings-header {
padding: 20px;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, var(--light-bg) 0%, #eef5ff 100%);
}
.settings-title {
font-size: 20px;
font-weight: 700;
color: var(--text);
margin-bottom: 5px;
}
.settings-subtitle {
font-size: 14px;
color: var(--text-light);
}
.settings-content {
padding: 20px;
flex-grow: 1;
overflow-y: auto;
}
.settings-group {
margin-bottom: 30px;
background-color: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.settings-group-title {
font-size: 16px;
font-weight: 600;
color: var(--text);
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary);
display: flex;
align-items: center;
gap: 10px;
}
.settings-group-title i {
color: var(--primary);
}
.setting-item {
margin-bottom: 18px;
}
.setting-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--text);
}
.setting-description {
font-size: 12px;
color: var(--text-light);
margin-top: 4px;
line-height: 1.4;
}
.setting-control {
width: 100%;
padding: 10px 14px;
background-color: var(--light-bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
transition: all 0.2s;
}
.setting-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.1);
}
.slider-container {
display: flex;
align-items: center;
gap: 12px;
}
input[type="range"] {
flex: 1;
height: 6px;
-webkit-appearance: none;
background: var(--border);
border-radius: 3px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
border: 3px solid white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}
.slider-value {
min-width: 40px;
text-align: center;
font-size: 14px;
font-weight: 600;
color: var(--primary);
background-color: var(--light-bg);
padding: 4px 8px;
border-radius: 4px;
}
.checkbox-container {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
padding: 8px 0;
}
.checkbox-container input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.checkbox-container label {
font-size: 14px;
color: var(--text);
cursor: pointer;
flex-grow: 1;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--primary);
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.settings-footer {
padding: 20px;
border-top: 1px solid var(--border);
background: linear-gradient(135deg, var(--light-bg) 0%, #eef5ff 100%);
}
.settings-btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border: none;
color: white;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11);
}
.settings-btn:hover {
transform: translateY(-2px);
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1);
}
.settings-btn.secondary {
background: linear-gradient(135deg, var(--light-bg) 0%, #e0e0e0 100%);
color: var(--text);
border: 1px solid var(--border);
margin-top: 10px;
}
.settings-btn.secondary:hover {
background: linear-gradient(135deg, #e0e0e0 0%, #d0d0d0 100%);
}
/* 提示信息 */
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.alert-info {
background: linear-gradient(135deg, rgba(74, 144, 226, 0.1) 0%, rgba(74, 144, 226, 0.2) 100%);
border-left: 4px solid var(--primary);
color: var(--primary);
}
.alert-success {
background: linear-gradient(135deg, rgba(52, 199, 89, 0.1) 0%, rgba(52, 199, 89, 0.2) 100%);
border-left: 4px solid var(--success);
color: var(--success);
}
.alert-warning {
background: linear-gradient(135deg, rgba(255, 149, 0, 0.1) 0%, rgba(255, 149, 0, 0.2) 100%);
border-left: 4px solid var(--warning);
color: var(--warning);
}
.alert-danger {
background: linear-gradient(135deg, rgba(255, 59, 48, 0.1) 0%, rgba(255, 59, 48, 0.2) 100%);
border-left: 4px solid var(--danger);
color: var(--danger);
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c0c0c0;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a0a0a0;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.sidebar {
width: 80px;
}
.menu-item span, .logo h1, .logo .version, .status-item span:first-child {
display: none;
}
.logo {
padding: 20px 10px;
}
.menu-item {
justify-content: center;
padding: 15px 0;
}
.menu-item i {
margin-right: 0;
font-size: 18px;
}
.status-bar {
padding: 15px 10px;
}
.settings-panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 100%;
max-width: 400px;
z-index: 100;
}
}
@media (max-width: 768px) {
.message {
max-width: 95%;
}
.header {
padding: 12px 16px;
}
.header-actions {
flex-direction: column;
gap: 5px;
}
.header-btn {
padding: 6px 10px;
font-size: 12px;
}
}
/* 打字机效果 */
.typing-indicator {
display: inline-flex;
align-items: center;
padding: 12px 16px;
background: linear-gradient(135deg, var(--light-bg) 0%, #f0f0f0 100%);
border-radius: 12px;
font-size: 14px;
color: var(--text-light);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.typing-dots {
display: inline-flex;
margin-left: 5px;
}
.typing-dots span {
width: 4px;
height: 4px;
border-radius: 50%;
background-color: var(--text-light);
margin: 0 1px;
animation: typing 1.4s infinite both;
}
.typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-5px); }
}
/* Markdown渲染样式 */
.markdown-content {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
line-height: 1.6;
overflow-wrap: break-word;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 1.2em;
margin-bottom: 0.6em;
font-weight: 600;
line-height: 1.25;
}
.markdown-content h1 {
font-size: 1.8em;
border-bottom: 2px solid var(--border);
padding-bottom: 0.3em;
margin-top: 0.8em;
}
.markdown-content h2 {
font-size: 1.5em;
border-bottom: 1px solid var(--border);
padding-bottom: 0.3em;
margin-top: 1em;
}
.markdown-content h3 {
font-size: 1.3em;
margin-top: 1.2em;
}
.markdown-content h4 { font-size: 1.1em; }
.markdown-content h5 { font-size: 1em; color: var(--text-light); }
.markdown-content h6 { font-size: 0.9em; color: var(--text-light); }
.markdown-content p {
margin-bottom: 1em;
line-height: 1.6;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1em;
padding-left: 2em;
}
.markdown-content li {
margin-bottom: 0.5em;
line-height: 1.5;
}
.markdown-content li > ul,
.markdown-content li > ol {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.markdown-content blockquote {
margin: 0 0 1em 0;
padding: 12px 16px;
color: var(--text-light);
border-left: 4px solid var(--border);
font-style: italic;
background-color: rgba(0, 0, 0, 0.02);
border-radius: 0 8px 8px 0;
}
.markdown-content code {
background-color: rgba(0, 0, 0, 0.05);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
}
.markdown-content pre {
background: linear-gradient(135deg, var(--code-bg) 0%, #253b5c 100%);
color: var(--code-color);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 1em 0;
font-size: 0.9em;
line-height: 1.5;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
position: relative;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
font-size: 1em;
color: inherit;
}
.markdown-content table {
border-collapse: collapse;
width: 100%;
margin-bottom: 1em;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--border);
}
.markdown-content th,
.markdown-content td {
border: 1px solid var(--border);
padding: 12px 16px;
text-align: left;
}
.markdown-content th {
background: linear-gradient(135deg, var(--light-bg) 0%, #e8e8e8 100%);
font-weight: 600;
}
.markdown-content tr:nth-child(even) {
background-color: rgba(0, 0, 0, 0.02);
}
.markdown-content a {
color: var(--primary);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: all 0.2s;
}
.markdown-content a:hover {
border-bottom: 1px solid var(--primary);
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 1em 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.markdown-content hr {
border: none;
border-top: 1px solid var(--border);
margin: 2em 0;
}
.markdown-content strong, .markdown-content b {
font-weight: 600;
color: var(--text);
}
.markdown-content em, .markdown-content i {
font-style: italic;
}
/* 代码块语言标签 */
.code-language {
display: inline-block;
background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%);
color: #bdc3c7;
padding: 6px 12px;
border-radius: 6px 6px 0 0;
font-size: 12px;
margin-bottom: -10px;
position: relative;
z-index: 1;
font-weight: 600;
}
/* KaTeX LaTeX样式 */
.katex {
font-size: 1.1em !important;
}
.katex-display {
overflow-x: auto;
overflow-y: hidden;
padding: 1em 0;
margin: 1em 0;
text-align: center;
}
.katex-display > .katex {
white-space: nowrap;
}
/* 数学公式特殊样式 */
.katex .mfrac {
padding: 0 0.2em;
}
/* 流式渲染进度指示器 */
.streaming-indicator {
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--primary);
border-radius: 50%;
margin-left: 4px;
animation: pulse 1.5s infinite;
vertical-align: middle;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* 费用显示 */
.cost-display {
display: flex;
justify-content: space-between;
background-color: var(--light-bg);
padding: 12px 16px;
border-radius: 8px;
margin-top: 15px;
font-size: 13px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
</style>
</head>
<body>
<div class="app-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="logo">
<h1>DeepSeek</h1>
<div class="version">Web客户端 v1.6</div>
</div>
<div class="menu">
<div class="menu-section">
<h3>工具</h3>
<div class="menu-item" id="settingsBtn">
<i class="fas fa-cog"></i>
<span>设置</span>
</div>
</div>
<div class="menu-section">
<h3>对话管理</h3>
<div class="menu-item" id="newChatBtn">
<i class="fas fa-plus-circle"></i>
<span>新对话</span>
</div>
<div class="menu-item" id="saveChatBtn">
<i class="fas fa-save"></i>
<span>保存对话</span>
</div>
<div class="menu-item" id="loadChatBtn">
<i class="fas fa-folder-open"></i>
<span>加载对话</span>
</div>
</div>
</div>
<div class="status-bar">
<div class="status-item">
<span>总费用:</span>
<span class="status-value" id="totalCost">0.00 元</span>
</div>
<div class="status-item">
<span>总Tokens:</span>
<span class="status-value" id="totalTokens">0</span>
</div>
<div class="status-item">
<span>模型:</span>
<span class="status-value" id="currentModel">deepseek-chat</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="header">
<div class="chat-title">DeepSeek对话</div>
<div class="header-actions">
<button class="header-btn" id="showParamsBtn">
<i class="fas fa-sliders-h"></i>
设置
</button>
<button class="header-btn" id="clearChatBtn">
<i class="fas fa-trash-alt"></i>
清空
</button>
</div>
</div>
<div class="chat-container">
<div class="messages-container" id="messagesContainer">
<div class="empty-chat" id="emptyChat">
<i class="fas fa-robot"></i>
<h3>欢迎使用DeepSeek Web客户端</h3>
<p>这是一个功能完整的DeepSeek API客户端,支持流式响应、参数调节、费用计算等功能。</p>
<p style="margin-top: 20px;">请在侧边栏设置中配置您的API密钥以开始使用。</p>
</div>
</div>
<div class="input-container">
<div class="input-area">
<textarea class="message-input" id="messageInput" placeholder="输入消息,Enter换行,Ctrl+Enter发送" rows="3"></textarea>
<button class="send-button" id="sendButton" disabled>
<i class="fas fa-paper-plane"></i>
发送
</button>
</div>
<div class="input-actions">
<button class="action-btn" id="toggleReasoningBtn" title="深度思考模式">
<i class="fas fa-brain"></i>
深度思考: <span id="reasoningStatus">关闭</span>
</button>
<button class="action-btn" id="toggleAutoRenderBtn" title="自动Markdown渲染">
<i class="fas fa-code"></i>
Markdown渲染: <span id="renderStatus">开启</span>
</button>
<button class="action-btn" id="toggleLatexBtn" title="LaTeX公式渲染">
<i class="fas fa-square-root-alt"></i>
LaTeX渲染: <span id="latexStatus">开启</span>
</button>
</div>
<div class="input-hint">提示:输入消息后按Ctrl+Enter发送,Enter键换行</div>
</div>
</div>
</div>
<!-- 设置面板 -->
<div class="settings-panel" id="settingsPanel">
<div class="settings-header">
<div class="settings-title">DeepSeek设置</div>
<div class="settings-subtitle">配置API密钥和模型参数</div>
</div>
<div class="settings-content">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
请从DeepSeek官网获取API密钥并在此配置
</div>
<div class="settings-group">
<div class="settings-group-title">
<i class="fas fa-key"></i>
API配置
</div>
<div class="setting-item">
<label class="setting-label">API密钥</label>
<input type="password" class="setting-control" id="apiKey" placeholder="输入DeepSeek API密钥">
<div class="setting-description">
密钥长度应为35个字符。您可以在DeepSeek平台获取API密钥。
</div>
</div>
<div class="setting-item">
<label class="setting-label">模型选择</label>
<select class="setting-control" id="modelSelect">
<option value="deepseek-chat">deepseek-chat (标准)</option>
<option value="deepseek-reasoner">deepseek-reasoner (深度思考)</option>
</select>
<div class="setting-description">
深度思考模型提供更详细的推理过程,但价格更高。
</div>
</div>
</div>
<div class="settings-group">
<div class="settings-group-title">
<i class="fas fa-sliders-h"></i>
生成参数
</div>
<div class="setting-item">
<label class="setting-label">温度 (Temperature)</label>
<div class="slider-container">
<input type="range" class="setting-control" id="temperature" min="0" max="2" step="0.1" value="0.7">
<span class="slider-value" id="temperatureValue">0.7</span>
</div>
<div class="setting-description">
控制输出的随机性。值越高,输出越随机;值越低,输出越确定。
</div>
</div>
<div class="setting-item">
<label class="setting-label">核采样 (Top P)</label>
<div class="slider-container">
<input type="range" class="setting-control" id="topP" min="0" max="1" step="0.05" value="1">
<span class="slider-value" id="topPValue">1</span>
</div>
<div class="setting-description">
控制核采样方法的概率阈值。通常与温度参数一起使用。
</div>
</div>
<div class="setting-item">
<label class="setting-label">频率惩罚 (Frequency Penalty)</label>
<div class="slider-container">
<input type="range" class="setting-control" id="frequencyPenalty" min="-2" max="2" step="0.1" value="0">
<span class="slider-value" id="frequencyPenaltyValue">0</span>
</div>
<div class="setting-description">
正值减少重复token的出现,负值增加重复token的出现。
</div>
</div>
<div class="setting-item">
<label class="setting-label">存在惩罚 (Presence Penalty)</label>
<div class="slider-container">
<input type="range" class="setting-control" id="presencePenalty" min="-2" max="2" step="0.1" value="0">
<span class="slider-value" id="presencePenaltyValue">0</span>
</div>
<div class="setting-description">
正值鼓励模型谈论新话题,负值鼓励模型重复已提及的话题。
</div>
</div>
<div class="setting-item">
<label class="setting-label">最大生成长度 (Max Tokens)</label>
<div class="slider-container">
<input type="range" class="setting-control" id="maxTokens" min="100" max="4096" step="100" value="2048">
<span class="slider-value" id="maxTokensValue">2048</span>
</div>
<div class="setting-description">
控制API响应的最大长度。注意:上下文总长度有限制。
</div>
</div>
</div>
<!-- 系统消息设置 -->
<div class="settings-group">
<div class="settings-group-title">
<i class="fas fa-comment"></i>
系统消息
</div>
<div class="setting-item">
<label class="setting-label">系统消息(可选)</label>
<textarea class="setting-control" id="systemPrompt" rows="4" placeholder="输入系统消息,用于设置AI的行为背景(例如:你是一个有用的助手)"></textarea>
<div class="setting-description">
系统消息将在每次对话开始时作为背景设定发送给AI。
</div>
</div>
</div>
</div>
<div class="settings-footer">
<button class="settings-btn" id="saveSettingsBtn">
保存设置
</button>
<button class="settings-btn secondary" id="closeSettingsBtn">
关闭
</button>
</div>
</div>
</div>
<!-- KaTeX JS for LaTeX rendering -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js" crossorigin="anonymous"></script>
<script>
// DeepSeek Web客户端
class DeepSeekClient {
constructor() {
// 状态变量
this.isDebugMode = false;
this.useReasoningModel = false;
this.autoRender = true;
this.enableLatex = true;
this.totalCost = 0;
this.totalTokens = 0;
this.isLoading = false;
this.isStreaming = false;
this.systemPrompt = '';
this.hasSystemMessageInConversation = false;
// 当前对话
this.currentConversation = {
id: this.generateId(),
title: '新对话',
messages: [],
timestamp: Date.now(),
usage: {
prompt_cache_hit_tokens: 0,
prompt_cache_miss_tokens: 0,
completion_tokens: 0,
total_tokens: 0
}
};
// 对话历史
this.conversations = [];
// 参数默认值
this.params = {
temperature: 0.7,
topP: 1,
frequencyPenalty: 0,
presencePenalty: 0,
maxTokens: 2048,
model: 'deepseek-chat'
};
// API价格(元/百万token)
this.prices = {
cacheHit: 0.2,
cacheMiss: 2,
output: 3
};
// 初始化
this.init();
}
// 初始化应用
init() {
this.loadSettings();
this.loadConversations();
this.setupEventListeners();
this.updateUI();
console.log('DeepSeek Web客户端已初始化');
}
// 生成唯一ID
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 加载设置
loadSettings() {
const settings = localStorage.getItem('deepseek_settings');
if (settings) {
try {
const parsed = JSON.parse(settings);
this.params = { ...this.params, ...parsed.params };
this.totalCost = parsed.totalCost || 0;
this.totalTokens = parsed.totalTokens || 0;
this.autoRender = parsed.autoRender !== undefined ? parsed.autoRender : true;
this.enableLatex = parsed.enableLatex !== undefined ? parsed.enableLatex : true;
this.systemPrompt = parsed.systemPrompt || '';
// 更新UI元素
document.getElementById('apiKey').value = parsed.apiKey || '';
document.getElementById('modelSelect').value = this.params.model;
document.getElementById('systemPrompt').value = this.systemPrompt;
document.getElementById('temperature').value = this.params.temperature;
document.getElementById('temperatureValue').textContent = this.params.temperature;
document.getElementById('topP').value = this.params.topP;
document.getElementById('topPValue').textContent = this.params.topP;
document.getElementById('frequencyPenalty').value = this.params.frequencyPenalty;
document.getElementById('frequencyPenaltyValue').textContent = this.params.frequencyPenalty;
document.getElementById('presencePenalty').value = this.params.presencePenalty;
document.getElementById('presencePenaltyValue').textContent = this.params.presencePenalty;
document.getElementById('maxTokens').value = this.params.maxTokens;
document.getElementById('maxTokensValue').textContent = this.params.maxTokens;
// 更新状态栏
document.getElementById('totalCost').textContent = this.totalCost.toFixed(6) + ' 元';
document.getElementById('totalTokens').textContent = this.totalTokens;
document.getElementById('currentModel').textContent = this.params.model;
// 更新按钮状态
document.getElementById('renderStatus').textContent = this.autoRender ? '开启' : '关闭';
document.getElementById('reasoningStatus').textContent = this.useReasoningModel ? '开启' : '关闭';
document.getElementById('latexStatus').textContent = this.enableLatex ? '开启' : '关闭';
// 启用发送按钮(如果有API密钥)
if (parsed.apiKey && parsed.apiKey.length === 35) {
document.getElementById('sendButton').disabled = false;
}
} catch (e) {
console.error('加载设置时出错:', e);
}
}
}
// 保存设置
saveSettings() {
const settings = {
apiKey: document.getElementById('apiKey').value,
params: this.params,
totalCost: this.totalCost,
totalTokens: this.totalTokens,
autoRender: this.autoRender,
enableLatex: this.enableLatex,
systemPrompt: this.systemPrompt
};
localStorage.setItem('deepseek_settings', JSON.stringify(settings));
// 如果API密钥有效,启用发送按钮
if (settings.apiKey && settings.apiKey.length === 35) {
document.getElementById('sendButton').disabled = false;
} else {
document.getElementById('sendButton').disabled = true;
}
this.showAlert('设置已保存', 'success');
}
// 加载对话历史
loadConversations() {
const conversations = localStorage.getItem('deepseek_conversations');
if (conversations) {
try {
this.conversations = JSON.parse(conversations);
} catch (e) {
console.error('加载对话历史时出错:', e);
}
}
}
// 保存对话历史
saveConversations() {
localStorage.setItem('deepseek_conversations', JSON.stringify(this.conversations));
}
// 设置事件监听器
setupEventListeners() {
// 发送消息
document.getElementById('sendButton').addEventListener('click', () => this.sendMessage());
// 输入框快捷键:Ctrl+Enter发送,Enter换行
document.getElementById('messageInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault();
this.sendMessage();
}
});
// 输入框自动调整高度
document.getElementById('messageInput').addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
});
// 切换设置面板
document.getElementById('showParamsBtn').addEventListener('click', () => {
document.getElementById('settingsPanel').classList.toggle('open');
});
document.getElementById('settingsBtn').addEventListener('click', () => {
document.getElementById('settingsPanel').classList.toggle('open');
});
// 关闭设置面板
document.getElementById('closeSettingsBtn').addEventListener('click', () => {
document.getElementById('settingsPanel').classList.remove('open');
});
// 保存设置
document.getElementById('saveSettingsBtn').addEventListener('click', () => {
this.updateParamsFromUI();
this.systemPrompt = document.getElementById('systemPrompt').value;
this.saveSettings();
document.getElementById('settingsPanel').classList.remove('open');
});
// 清空对话
document.getElementById('clearChatBtn').addEventListener('click', () => {
if (confirm('确定要清空当前对话吗?')) {
this.newConversation();
}
});
// 新对话
document.getElementById('newChatBtn').addEventListener('click', () => {
this.newConversation();
});
// 保存对话
document.getElementById('saveChatBtn').addEventListener('click', () => {
this.saveConversationToFile();
});
// 加载对话
document.getElementById('loadChatBtn').addEventListener('click', () => {
this.loadConversationFromFile();
});
// 切换深度思考模式
document.getElementById('toggleReasoningBtn').addEventListener('click', () => {
this.toggleReasoningModel();
});
// 切换Markdown渲染
document.getElementById('toggleAutoRenderBtn').addEventListener('click', () => {
this.toggleAutoRender();
});
// 切换LaTeX渲染
document.getElementById('toggleLatexBtn').addEventListener('click', () => {
this.toggleLatex();
});
// 参数滑块事件
this.setupSliderEvents();
}
// 设置滑块事件
setupSliderEvents() {
const sliders = [
{ id: 'temperature', valueId: 'temperatureValue' },
{ id: 'topP', valueId: 'topPValue' },
{ id: 'frequencyPenalty', valueId: 'frequencyPenaltyValue' },
{ id: 'presencePenalty', valueId: 'presencePenaltyValue' },
{ id: 'maxTokens', valueId: 'maxTokensValue' }
];
sliders.forEach(slider => {
const element = document.getElementById(slider.id);
const valueElement = document.getElementById(slider.valueId);
element.addEventListener('input', () => {
valueElement.textContent = element.value;
});
});
}
// 从UI更新参数
updateParamsFromUI() {
this.params.temperature = parseFloat(document.getElementById('temperature').value);
this.params.topP = parseFloat(document.getElementById('topP').value);
this.params.frequencyPenalty = parseFloat(document.getElementById('frequencyPenalty').value);
this.params.presencePenalty = parseFloat(document.getElementById('presencePenalty').value);
this.params.maxTokens = parseInt(document.getElementById('maxTokens').value);
this.params.model = document.getElementById('modelSelect').value;
// 更新状态栏
document.getElementById('currentModel').textContent = this.params.model;
console.log('参数已更新:', this.params);
}
// ==================== 新增:编辑与重新生成功能 ====================
// 为消息元素添加编辑和重新生成按钮
enhanceMessageElement(element, role, messageId) {
const header = element.querySelector('.message-header');
if (!header) return;
// 创建按钮容器
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
// 编辑按钮(所有消息类型都添加)
const editBtn = document.createElement('button');
editBtn.className = 'message-action-btn';
editBtn.title = '编辑消息';
editBtn.innerHTML = '<i class="fas fa-edit"></i>';
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.enterEditMode(element, messageId, role);
});
actionsDiv.appendChild(editBtn);
// 重新生成按钮(仅AI消息)
if (role === 'assistant') {
const regenBtn = document.createElement('button');
regenBtn.className = 'message-action-btn';
regenBtn.title = '重新生成';
regenBtn.innerHTML = '<i class="fas fa-sync-alt"></i>';
regenBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.regenerateMessage(messageId);
});
actionsDiv.appendChild(regenBtn);
}
header.appendChild(actionsDiv);
// 为AI头像添加重新生成功能
if (role === 'assistant') {
const avatar = element.querySelector('.message-avatar');
if (avatar) {
avatar.addEventListener('click', (e) => {
e.stopPropagation();
this.regenerateMessage(messageId);
});
}
}
}
// 进入编辑模式
enterEditMode(messageElement, messageId, role) {
const contentElement = messageElement.querySelector('.message-content');
if (!contentElement) return;
const originalContent = contentElement.dataset.originalText || contentElement.textContent;
// 保存原始内容以备取消
contentElement.dataset.originalContent = originalContent;
// 替换为编辑界面
const textarea = document.createElement('textarea');
textarea.className = 'edit-textarea';
textarea.value = originalContent;
textarea.rows = Math.min(10, originalContent.split('\n').length + 2);
const saveBtn = document.createElement('button');
saveBtn.className = 'edit-btn edit-save';
saveBtn.textContent = '保存';
saveBtn.addEventListener('click', () => {
this.saveEdit(messageElement, messageId, textarea.value, role);
});
const cancelBtn = document.createElement('button');
cancelBtn.className = 'edit-btn edit-cancel';
cancelBtn.textContent = '取消';
cancelBtn.addEventListener('click', () => {
this.cancelEdit(messageElement, originalContent);
});
const editActions = document.createElement('div');
editActions.className = 'edit-actions';
editActions.appendChild(saveBtn);
editActions.appendChild(cancelBtn);
// 清空内容区域并添加编辑控件
contentElement.innerHTML = '';
contentElement.appendChild(textarea);
contentElement.appendChild(editActions);
}
// 保存编辑
saveEdit(messageElement, messageId, newContent, role) {
// 更新数组中的消息内容
const msgIndex = this.currentConversation.messages.findIndex(m => m.messageId === messageId);
if (msgIndex === -1) return;
const originalMsg = this.currentConversation.messages[msgIndex];
if (originalMsg.content === newContent) {
// 内容未变,直接退出编辑
this.cancelEdit(messageElement, newContent);
return;
}
// 更新消息内容
this.currentConversation.messages[msgIndex].content = newContent;
// 删除该消息之后的所有消息(因为历史改变了)
this.deleteMessagesAfter(messageId);
// 重新渲染该消息的内容
const contentElement = messageElement.querySelector('.message-content');
contentElement.innerHTML = ''; // 移除编辑控件
if (role === 'assistant' && this.autoRender) {
this.renderAndSetContent(contentElement, newContent);
} else {
contentElement.textContent = newContent;
if (role === 'assistant') contentElement.classList.add('raw-text');
}
// 滚动到底部
this.scrollToBottom();
this.showAlert('消息已更新', 'success');
// 根据角色自动触发后续回复
if (role === 'user') {
// 编辑的是用户消息,从该消息继续生成AI回复
this.continueFromMessage(messageId);
} else if (role === 'assistant') {
// 编辑的是AI消息,重新生成该消息
this.regenerateMessage(messageId);
}
}
// 取消编辑
cancelEdit(messageElement, originalContent) {
const contentElement = messageElement.querySelector('.message-content');
contentElement.innerHTML = ''; // 移除编辑控件
contentElement.textContent = originalContent;
// 恢复可能的渲染类
if (messageElement.classList.contains('assistant-message') && this.autoRender) {
this.renderAndSetContent(contentElement, originalContent);
}
}
// 删除某条消息之后的所有消息(从数组和DOM)
deleteMessagesAfter(messageId) {
const startIndex = this.currentConversation.messages.findIndex(m => m.messageId === messageId);
if (startIndex === -1) return;
// 获取所有后续消息的ID
const afterMessages = this.currentConversation.messages.slice(startIndex + 1);
const afterIds = afterMessages.map(m => m.messageId);
// 从DOM中移除这些消息元素
afterIds.forEach(id => {
const elem = document.querySelector(`.message[data-message-id="${id}"]`);
if (elem) elem.remove();
});
// 从数组中截断
this.currentConversation.messages = this.currentConversation.messages.slice(0, startIndex + 1);
}
// 重新生成AI消息
async regenerateMessage(messageId) {
const index = this.currentConversation.messages.findIndex(m => m.messageId === messageId);
if (index === -1 || this.currentConversation.messages[index].role !== 'assistant') return;
// 检查API密钥
const apiKey = document.getElementById('apiKey').value;
if (!apiKey || apiKey.length !== 35) {
this.showAlert('请先配置有效的API密钥', 'danger');
return;
}
// 禁用发送按钮
this.isLoading = true;
this.isStreaming = true;
document.getElementById('sendButton').disabled = true;
// 删除该消息之后的所有消息
this.deleteMessagesAfter(messageId);
// 获取消息元素
const messageElement = document.querySelector(`.message[data-message-id="${messageId}"]`);
if (!messageElement) return;
const contentElement = messageElement.querySelector('.message-content');
if (!contentElement) return;
// 清空当前内容,显示加载指示器
contentElement.innerHTML = '';
const loadingDiv = document.createElement('div');
loadingDiv.className = 'typing-indicator';
loadingDiv.innerHTML = '重新生成中 <div class="typing-dots"><span></span><span></span><span></span></div>';
contentElement.appendChild(loadingDiv);
// 准备上下文消息(该消息之前的所有消息)
const contextMessages = this.currentConversation.messages.slice(0, index); // 不包含当前消息
try {
// 调用流式生成,更新当前元素
await this.streamToElement(messageElement, contextMessages, {
onFinish: (usage) => {
if (usage) {
this.updateUsage(usage);
}
// 重新启用发送按钮
this.isLoading = false;
this.isStreaming = false;
document.getElementById('sendButton').disabled = false;
}
});
} catch (error) {
console.error('重新生成失败:', error);
this.showAlert('重新生成失败: ' + error.message, 'danger');
// 恢复原内容?简单显示错误
contentElement.innerHTML = '生成失败,请重试。';
this.isLoading = false;
this.isStreaming = false;
document.getElementById('sendButton').disabled = false;
}
}
// 从某条用户消息继续生成(编辑用户消息后调用)
async continueFromMessage(messageId) {
const index = this.currentConversation.messages.findIndex(m => m.messageId === messageId);
if (index === -1) return;
const userMsg = this.currentConversation.messages[index];
if (userMsg.role !== 'user') return;
// 检查API密钥
const apiKey = document.getElementById('apiKey').value;
if (!apiKey || apiKey.length !== 35) {
this.showAlert('请先配置有效的API密钥', 'danger');
return;
}
this.isLoading = true;
this.isStreaming = true;
document.getElementById('sendButton').disabled = true;
// 创建新的AI消息元素
const aiMsgId = this.generateId();
const aiMessageElement = this.createMessageElement('', 'assistant', aiMsgId);
document.getElementById('messagesContainer').appendChild(aiMessageElement);
this.scrollToBottom();
// 上下文:包括该用户消息及之前的所有消息
const contextMessages = this.currentConversation.messages.slice(0, index + 1);
try {
await this.streamToElement(aiMessageElement, contextMessages, {
onFinish: (usage) => {
if (usage) this.updateUsage(usage);
// 将生成的AI消息加入数组
const content = aiMessageElement.querySelector('.message-content').dataset.originalText || '';
this.currentConversation.messages.push({
role: 'assistant',
content: content,
reasoning: aiMessageElement.querySelector('.reasoning-content')?.textContent || '',
timestamp: Date.now(),
usage: usage,
messageId: aiMsgId
});
this.isLoading = false;
this.isStreaming = false;
document.getElementById('sendButton').disabled = false;
}
});
} catch (error) {
console.error('继续生成失败:', error);
this.showAlert('继续生成失败: ' + error.message, 'danger');
aiMessageElement.remove();
this.isLoading = false;
this.isStreaming = false;
document.getElementById('sendButton').disabled = false;
}
}
// 通用的流式生成方法,将结果渲染到指定的消息元素中(元素已存在)
async streamToElement(messageElement, contextMessages, options = {}) {
const apiKey = document.getElementById('apiKey').value;
const contentElement = messageElement.querySelector('.message-content');
if (!contentElement) throw new Error('消息内容元素不存在');
// 准备请求体
const requestBody = {
model: this.params.model,
messages: this.prepareMessagesFromContext(contextMessages),
temperature: this.params.temperature,
top_p: this.params.topP,
frequency_penalty: this.params.frequencyPenalty,
presence_penalty: this.params.presencePenalty,
max_tokens: this.params.maxTokens,
stream: true
};
if (this.isDebugMode) {
console.log('API请求:', requestBody);
}
const response = await fetch('https://api.deepseek.com/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
let reasoningContent = '';
let isReasoning = false;
let usageData = null;
let lastRenderTime = 0;
const renderInterval = 50;
// 清空contentElement,准备接收流式内容
contentElement.innerHTML = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6);
if (data === '[DONE]') break;
try {
const parsed = JSON.parse(data);
if (parsed.usage) {
usageData = parsed.usage;
}
if (parsed.choices && parsed.choices[0].delta.reasoning_content) {
isReasoning = true;
reasoningContent += parsed.choices[0].delta.reasoning_content;
this.updateReasoningDisplay(messageElement, reasoningContent);
}
if (parsed.choices && parsed.choices[0].delta.content) {
if (isReasoning) {
isReasoning = false;
fullResponse += '\n\n';
}
fullResponse += parsed.choices[0].delta.content;
const now = Date.now();
if (now - lastRenderTime > renderInterval) {
this.updateMessageContentStreaming(messageElement, fullResponse, reasoningContent);
lastRenderTime = now;
}
this.scrollToBottom();
}
} catch (e) {
console.error('解析流数据失败:', e);
}
}
}
}
// 最终更新
this.finalRenderMessage(messageElement, fullResponse, reasoningContent);
// 更新数组中的消息内容(messageId对应的消息)
const messageId = messageElement.dataset.messageId;
const msgIndex = this.currentConversation.messages.findIndex(m => m.messageId === messageId);
if (msgIndex !== -1) {
this.currentConversation.messages[msgIndex].content = fullResponse;
this.currentConversation.messages[msgIndex].reasoning = reasoningContent;
this.currentConversation.messages[msgIndex].usage = usageData;
}
if (options.onFinish) options.onFinish(usageData);
}
// 根据上下文消息数组准备API messages(处理系统消息)
prepareMessagesFromContext(contextMessages) {
const messages = [];
if (this.systemPrompt && !this.hasSystemMessageInConversation) {
messages.push({ role: 'system', content: this.systemPrompt });
}
contextMessages.forEach(msg => {
messages.push({ role: msg.role, content: msg.content });
});
return messages;
}
// 发送消息(修改,使用streamToElement)
async sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message) {
this.showAlert('请输入消息', 'warning');
return;
}
const apiKey = document.getElementById('apiKey').value;
if (!apiKey || apiKey.length !== 35) {
this.showAlert('请先配置有效的API密钥', 'danger');
document.getElementById('settingsPanel').classList.add('open');
return;
}
this.isLoading = true;
this.isStreaming = true;
document.getElementById('sendButton').disabled = true;
const emptyChat = document.getElementById('emptyChat');
if (emptyChat) emptyChat.style.display = 'none';
if (this.currentConversation.messages.length === 0 && this.systemPrompt) {
this.addSystemMessage(this.systemPrompt);
this.hasSystemMessageInConversation = true;
}
// 添加用户消息
const userMsgId = this.generateId();
this.addMessageToUI(message, 'user', userMsgId);
this.currentConversation.messages.push({
role: 'user',
content: message,
timestamp: Date.now(),
messageId: userMsgId
});
input.value = '';
input.style.height = 'auto';
// 创建AI消息元素(但先不加入内容)
const aiMsgId = this.generateId();
const aiMessageElement = this.createMessageElement('', 'assistant', aiMsgId);
document.getElementById('messagesContainer').appendChild(aiMessageElement);
this.scrollToBottom();
// 准备上下文(包括刚添加的用户消息)
const contextMessages = this.currentConversation.messages.slice(); // 包含刚添加的用户消息
try {
await this.streamToElement(aiMessageElement, contextMessages, {
onFinish: (usage) => {
if (usage) {
this.updateUsage(usage);
}
// 将AI消息加入数组
const content = aiMessageElement.querySelector('.message-content').dataset.originalText || '';
this.currentConversation.messages.push({
role: 'assistant',
content: content,
reasoning: aiMessageElement.querySelector('.reasoning-content')?.textContent || '',
timestamp: Date.now(),
usage: usage,
messageId: aiMsgId
});
this.isLoading = false;
this.isStreaming = false;
document.getElementById('sendButton').disabled = false;
}
});
} catch (error) {
console.error('API调用失败:', error);
this.showAlert('API调用失败: ' + error.message, 'danger');
aiMessageElement.remove();
this.isLoading = false;
this.isStreaming = false;
document.getElementById('sendButton').disabled = false;
}
}
// ==================== 原有方法,需要适当修改 ====================
// 添加消息到UI(增加messageId参数)
addMessageToUI(content, role, messageId = null) {
if (!messageId) messageId = this.generateId();
const messageElement = this.createMessageElement(content, role, messageId);
document.getElementById('messagesContainer').appendChild(messageElement);
// 添加编辑/重新生成按钮
this.enhanceMessageElement(messageElement, role, messageId);
if (role === 'assistant' && this.autoRender) {
this.renderAndSetContent(messageElement.querySelector('.message-content'), content);
} else if (role === 'assistant' && !this.autoRender) {
const contentElement = messageElement.querySelector('.message-content');
contentElement.textContent = content;
contentElement.classList.add('raw-text');
} else if (role === 'system') {
const contentElement = messageElement.querySelector('.message-content');
contentElement.textContent = content;
}
this.scrollToBottom();
const emptyChat = document.getElementById('emptyChat');
if (emptyChat) emptyChat.style.display = 'none';
}
// 创建消息元素(增加messageId)
createMessageElement(content, role, messageId) {
const element = document.createElement('div');
element.className = `message ${role}-message`;
element.dataset.messageId = messageId;
const senderName = role === 'user' ? '用户' : (role === 'assistant' ? 'AI助手' : '系统');
const avatarClass = role === 'user' ? 'user-avatar' : (role === 'assistant' ? 'assistant-avatar' : 'system-avatar');
const senderClass = role === 'user' ? 'user-sender' : (role === 'assistant' ? 'assistant-sender' : 'system-sender');
const contentDiv = document.createElement('div');
contentDiv.className = `message-content ${role}-content`;
contentDiv.textContent = content;
element.innerHTML = `
<div class="message-header">
<div class="message-avatar ${avatarClass}">${role === 'user' ? '你' : (role === 'assistant' ? 'AI' : '系')}</div>
<div class="message-sender ${senderClass}">${senderName}</div>
<div class="message-time">${this.formatTime(new Date())}</div>
</div>
`;
element.appendChild(contentDiv);
return element;
}
// 更新推理内容显示
updateReasoningDisplay(messageElement, reasoningContent) {
let reasoningElement = messageElement.querySelector('.reasoning-content');
if (!reasoningElement) {
if (!reasoningContent) return;
reasoningElement = document.createElement('div');
reasoningElement.className = 'reasoning-content';
const contentElement = messageElement.querySelector('.message-content');
messageElement.insertBefore(reasoningElement, contentElement);
}
if (reasoningContent) {
reasoningElement.textContent = reasoningContent;
} else {
reasoningElement.remove();
}
}
// 流式更新消息内容(针对元素)
updateMessageContentStreaming(messageElement, content, reasoningContent) {
const contentElement = messageElement.querySelector('.message-content');
if (!this.autoRender) {
contentElement.textContent = content;
contentElement.classList.add('raw-text');
} else {
contentElement.classList.remove('raw-text');
contentElement.textContent = content;
}
this.addStreamingIndicator(contentElement);
}
// 添加流式传输指示器
addStreamingIndicator(contentElement) {
let indicator = contentElement.querySelector('.streaming-indicator');
if (!indicator) {
indicator = document.createElement('span');
indicator.className = 'streaming-indicator';
contentElement.appendChild(indicator);
}
}
// 移除流式传输指示器
removeStreamingIndicator(contentElement) {
const indicator = contentElement.querySelector('.streaming-indicator');
if (indicator) indicator.remove();
}
// 最终渲染消息
finalRenderMessage(messageElement, content, reasoningContent) {
const contentElement = messageElement.querySelector('.message-content');
if (this.autoRender) {
contentElement.classList.remove('raw-text');
this.renderAndSetContent(contentElement, content);
} else {
contentElement.textContent = content;
contentElement.classList.add('raw-text');
}
this.removeStreamingIndicator(contentElement);
if (!reasoningContent) {
const reasoningElement = messageElement.querySelector('.reasoning-content');
if (reasoningElement) reasoningElement.remove();
}
}
// 渲染并设置内容(完整版)
renderAndSetContent(contentElement, content) {
const originalText = content;
const html = this.renderMarkdown(content);
contentElement.innerHTML = `<div class="markdown-content">${html}</div>`;
contentElement.dataset.originalText = originalText;
if (this.enableLatex && window.renderMathInElement) {
setTimeout(() => {
try {
renderMathInElement(contentElement, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "$", right: "$", display: false},
{left: "\\(", right: "\\)", display: false},
{left: "\\[", right: "\\]", display: true},
{left: "\\begin{equation}", right: "\\end{equation}", display: true},
{left: "\\begin{equation*}", right: "\\end{equation*}", display: true},
{left: "\\begin{matrix}", right: "\\end{matrix}", display: true},
{left: "\\begin{bmatrix}", right: "\\end{bmatrix}", display: true},
{left: "\\begin{pmatrix}", right: "\\end{pmatrix}", display: true},
{left: "\\begin{vmatrix}", right: "\\end{vmatrix}", display: true},
{left: "\\begin{Vmatrix}", right: "\\end{Vmatrix}", display: true},
{left: "\\begin{align}", right: "\\end{align}", display: true},
{left: "\\begin{align*}", right: "\\end{align*}", display: true},
{left: "\\begin{aligned}", right: "\\end{aligned}", display: true},
{left: "\\begin{gather}", right: "\\end{gather}", display: true},
{left: "\\begin{gather*}", right: "\\end{gather*}", display: true},
{left: "\\begin{cases}", right: "\\end{cases}", display: true},
{left: "\\begin{array}", right: "\\end{array}", display: true},
{left: "\\begin{smallmatrix}", right: "\\end{smallmatrix}", display: true}
],
throwOnError: false,
errorColor: "#cc0000"
});
} catch (e) {
console.error('LaTeX渲染错误:', e);
}
}, 10);
}
}
// 渲染Markdown(保持原有)
renderMarkdown(text) {
let html = this.escapeHtml(text);
// 代码块
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const language = lang || 'text';
return `<div class="code-block"><span class="code-language">${language}</span><pre><code>${this.escapeHtml(code)}</code></pre></div>`;
});
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
html = html.replace(/^#{6} (.*$)/gim, '<h6>$1</h6>');
html = html.replace(/^#{5} (.*$)/gim, '<h5>$1</h5>');
html = html.replace(/^#{4} (.*$)/gim, '<h4>$1</h4>');
html = html.replace(/^#{3} (.*$)/gim, '<h3>$1</h3>');
html = html.replace(/^#{2} (.*$)/gim, '<h2>$1</h2>');
html = html.replace(/^#{1} (.*$)/gim, '<h1>$1</h1>');
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.*?)__/g, '<strong>$1</strong>');
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
html = html.replace(/_(.*?)_/g, '<em>$1</em>');
html = html.replace(/~~(.*?)~~/g, '<del>$1</del>');
html = html.replace(/^> (.*$)/gim, '<blockquote>$1</blockquote>');
html = html.replace(/^\s*[-*+] (.+)$/gim, '<li>$1</li>');
html = html.replace(/(<li>.*?<\/li>\s*)+/gs, '<ul>$&</ul>');
html = html.replace(/^\s*\d+\. (.+)$/gim, '<li>$1</li>');
html = html.replace(/(<li>.*?<\/li>\s*)+/gs, '<ol>$&</ol>');
html = html.replace(/^-{3,}$/gim, '<hr>');
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
html = html.replace(/!\[([^\]]+)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
const tableRegex = /\|(.+)\|[\r\n]+\|([-:| -]+)\|[\r\n]+((?:\|.+\|[\r\n]*)+)/g;
html = html.replace(tableRegex, (match, header, align, rows) => {
const headers = header.split('|').filter(h => h.trim()).map(h => `<th>${h.trim()}</th>`).join('');
const rowsArray = rows.trim().split('\n');
const rowsHtml = rowsArray.map(row => {
const cells = row.split('|').filter(c => c.trim()).map(c => `<td>${c.trim()}</td>`).join('');
return `<tr>${cells}</tr>`;
}).join('');
return `<table><thead><tr>${headers}</tr></thead><tbody>${rowsHtml}</tbody></table>`;
});
html = html.replace(/\n\n+/g, '</p><p>');
html = html.replace(/\n/g, '<br>');
html = '<p>' + html + '</p>';
html = html.replace(/<p><\/p>/g, '');
return html;
}
// 添加系统消息
addSystemMessage(content) {
const msgId = this.generateId();
this.currentConversation.messages.unshift({
role: 'system',
content: content,
timestamp: Date.now(),
messageId: msgId
});
this.addMessageToUI(content, 'system', msgId);
}
// 创建思考指示器(保持原有)
createThinkingIndicator() {
const element = document.createElement('div');
element.className = 'message assistant-message';
element.innerHTML = `
<div class="message-header">
<div class="message-avatar assistant-avatar">AI</div>
<div class="message-sender assistant-sender">AI助手</div>
<div class="message-time">${this.formatTime(new Date())}</div>
</div>
<div class="typing-indicator">
思考中
<div class="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
`;
return element;
}
// 更新使用量和费用
updateUsage(usage) {
this.currentConversation.usage = {
prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens || 0,
prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens || 0,
completion_tokens: usage.completion_tokens || 0,
total_tokens: usage.total_tokens || 0
};
const cost = this.calculateCost(usage);
this.totalCost += cost;
this.totalTokens += usage.total_tokens || 0;
document.getElementById('totalCost').textContent = this.totalCost.toFixed(6) + ' 元';
document.getElementById('totalTokens').textContent = this.totalTokens;
this.saveSettings();
}
// 计算费用
calculateCost(usage) {
const hitTokens = usage.prompt_cache_hit_tokens || 0;
const missTokens = usage.prompt_cache_miss_tokens || 0;
const outputTokens = usage.completion_tokens || 0;
const hitCost = (hitTokens / 1000000) * this.prices.cacheHit;
const missCost = (missTokens / 1000000) * this.prices.cacheMiss;
const outputCost = (outputTokens / 1000000) * this.prices.output;
return hitCost + missCost + outputCost;
}
// 切换深度思考模式
toggleReasoningModel() {
this.useReasoningModel = !this.useReasoningModel;
this.params.model = this.useReasoningModel ? 'deepseek-reasoner' : 'deepseek-chat';
document.getElementById('modelSelect').value = this.params.model;
document.getElementById('currentModel').textContent = this.params.model;
document.getElementById('reasoningStatus').textContent = this.useReasoningModel ? '开启' : '关闭';
this.showAlert(`深度思考模式已${this.useReasoningModel ? '开启' : '关闭'}`, 'info');
}
// 切换自动Markdown渲染
toggleAutoRender() {
this.autoRender = !this.autoRender;
document.getElementById('renderStatus').textContent = this.autoRender ? '开启' : '关闭';
this.showAlert(`Markdown自动渲染已${this.autoRender ? '开启' : '关闭'}`, 'info');
this.rerenderAllMessages();
this.saveSettings();
}
// 切换LaTeX渲染
toggleLatex() {
this.enableLatex = !this.enableLatex;
document.getElementById('latexStatus').textContent = this.enableLatex ? '开启' : '关闭';
this.showAlert(`LaTeX公式渲染已${this.enableLatex ? '开启' : '关闭'}`, 'info');
this.rerenderAllMessages();
this.saveSettings();
}
// 重新渲染所有消息
rerenderAllMessages() {
const messages = document.querySelectorAll('.message');
messages.forEach(messageElement => {
const contentElement = messageElement.querySelector('.message-content');
if (contentElement) {
const role = messageElement.classList.contains('user-message') ? 'user' :
messageElement.classList.contains('assistant-message') ? 'assistant' : 'system';
if (role === 'assistant') {
const originalText = contentElement.dataset.originalText || contentElement.textContent;
if (this.autoRender) {
this.renderAndSetContent(contentElement, originalText);
} else {
contentElement.textContent = originalText;
contentElement.classList.add('raw-text');
}
}
}
});
}
// 新对话
newConversation() {
if (this.currentConversation.messages.length > 0) {
this.saveCurrentConversationToLocal();
}
this.currentConversation = {
id: this.generateId(),
title: '新对话',
messages: [],
timestamp: Date.now(),
usage: {
prompt_cache_hit_tokens: 0,
prompt_cache_miss_tokens: 0,
completion_tokens: 0,
total_tokens: 0
}
};
this.hasSystemMessageInConversation = false;
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
const emptyChat = document.createElement('div');
emptyChat.className = 'empty-chat';
emptyChat.id = 'emptyChat';
emptyChat.innerHTML = `
<i class="fas fa-robot"></i>
<h3>新对话已开始</h3>
<p>输入消息开始与DeepSeek AI对话</p>
`;
container.appendChild(emptyChat);
this.showAlert('已开始新对话', 'success');
}
// 保存当前对话到本地存储
saveCurrentConversationToLocal() {
if (this.currentConversation.messages.length === 0) return;
const firstUserMessage = this.currentConversation.messages.find(msg => msg.role === 'user');
if (firstUserMessage) {
const title = firstUserMessage.content.substring(0, 30);
this.currentConversation.title = title.length < firstUserMessage.content.length ? title + '...' : title;
}
this.conversations.push({...this.currentConversation});
this.saveConversations();
}
// 保存对话到文件
saveConversationToFile() {
if (this.currentConversation.messages.length === 0) {
this.showAlert('当前对话为空,无需保存', 'warning');
return;
}
let text = '';
this.currentConversation.messages.forEach(msg => {
let rolePrefix = '';
switch (msg.role) {
case 'user': rolePrefix = 'u>'; break;
case 'assistant': rolePrefix = 'a>'; break;
case 'system': rolePrefix = 's>'; break;
}
text += `${rolePrefix}${msg.content}\n\n`;
});
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
a.download = `deepseek_chat_${timestamp}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showAlert('对话已保存为.txt文件', 'success');
}
// 从文件加载对话
loadConversationFromFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.txt';
input.style.display = 'none';
document.body.appendChild(input);
input.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
this.parseAndLoadConversation(e.target.result);
};
reader.readAsText(file);
document.body.removeChild(input);
});
input.click();
}
// 解析并加载对话
parseAndLoadConversation(content) {
this.newConversation();
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
const lines = content.split('\n');
let currentRole = '';
let currentContent = '';
const messages = [];
const rolePattern = /^(u>|a>|s>)(.*)/;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const match = line.match(rolePattern);
if (match) {
if (currentRole && currentContent) {
messages.push({
role: currentRole,
content: currentContent.trim()
});
}
const prefix = match[1];
switch (prefix) {
case 'u>': currentRole = 'user'; break;
case 'a>': currentRole = 'assistant'; break;
case 's>': currentRole = 'system'; break;
}
currentContent = match[2];
} else if (currentRole) {
currentContent += '\n' + line;
}
}
if (currentRole && currentContent) {
messages.push({
role: currentRole,
content: currentContent.trim()
});
}
const hasSystemMessage = messages.some(msg => msg.role === 'system');
if (hasSystemMessage) this.hasSystemMessageInConversation = true;
messages.forEach(msg => {
const msgId = this.generateId();
msg.messageId = msgId;
this.currentConversation.messages.push(msg);
this.addMessageToUI(msg.content, msg.role, msgId);
});
this.showAlert('对话已从文件加载', 'success');
}
// 更新UI
updateUI() {
document.getElementById('totalCost').textContent = this.totalCost.toFixed(6) + ' 元';
document.getElementById('totalTokens').textContent = this.totalTokens;
document.getElementById('currentModel').textContent = this.params.model;
document.getElementById('renderStatus').textContent = this.autoRender ? '开启' : '关闭';
document.getElementById('reasoningStatus').textContent = this.useReasoningModel ? '开启' : '关闭';
document.getElementById('latexStatus').textContent = this.enableLatex ? '开启' : '关闭';
}
// 显示提示信息
showAlert(message, type) {
const existingAlert = document.querySelector('.alert');
if (existingAlert) existingAlert.remove();
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'warning' ? 'exclamation-triangle' : type === 'danger' ? 'times-circle' : 'info-circle'}"></i>
${message}
`;
const settingsContent = document.querySelector('.settings-content');
if (settingsContent) {
settingsContent.insertBefore(alert, settingsContent.firstChild);
setTimeout(() => {
if (alert.parentNode) alert.remove();
}, 3000);
}
}
// 滚动到底部
scrollToBottom() {
const container = document.getElementById('messagesContainer');
container.scrollTop = container.scrollHeight;
}
// 格式化时间
formatTime(date) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// HTML转义
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// 初始化应用
const deepSeekClient = new DeepSeekClient();
window.deepSeekClient = deepSeekClient;
</script>
</body>
</html>