3688 lines
133 KiB
HTML
3688 lines
133 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>AI Chat Assistant</title>
|
||
<style>
|
||
/* Bot Selector Dropdown Styles */
|
||
.bot-selector {
|
||
position: relative;
|
||
}
|
||
|
||
.bot-selector-trigger {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
background: var(--background);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
min-width: 200px;
|
||
}
|
||
|
||
.bot-selector-trigger:hover {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.bot-selector-trigger.active {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.bot-selector-current {
|
||
flex: 1;
|
||
text-align: left;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.bot-selector-dropdown {
|
||
position: absolute;
|
||
top: calc(100% + 8px);
|
||
left: 0;
|
||
right: 0;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||
z-index: 100;
|
||
display: none;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.bot-selector-dropdown.show {
|
||
display: block;
|
||
}
|
||
|
||
.bot-selector-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 14px;
|
||
cursor: pointer;
|
||
transition: background-color 0.15s ease;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.bot-selector-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.bot-selector-item:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.bot-selector-item.selected {
|
||
background: rgba(37, 99, 235, 0.08);
|
||
}
|
||
|
||
.bot-selector-item-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.bot-selector-item-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.bot-selector-item-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.bot-selector-item-id {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
font-family: 'Monaco', 'Consolas', monospace;
|
||
}
|
||
|
||
.bot-selector-manage {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 14px;
|
||
border-top: 1px solid var(--border);
|
||
cursor: pointer;
|
||
color: var(--primary);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.bot-selector-manage:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
/* Bot Selection Modal */
|
||
.bot-selection-modal {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(4px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 200;
|
||
}
|
||
|
||
.bot-selection-modal.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.bot-selection-content {
|
||
background: var(--surface);
|
||
border-radius: 16px;
|
||
width: 90%;
|
||
max-width: 480px;
|
||
padding: 32px;
|
||
text-align: center;
|
||
}
|
||
|
||
.bot-selection-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
margin: 0 auto 20px;
|
||
border-radius: 16px;
|
||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
}
|
||
|
||
.bot-selection-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.bot-selection-text {
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.bot-selection-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* Settings read-only field */
|
||
.settings-input.readonly {
|
||
background: var(--background);
|
||
cursor: not-allowed;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.settings-input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.settings-input-copy {
|
||
position: absolute;
|
||
right: 8px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 6px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.settings-input-copy:hover {
|
||
background: var(--border);
|
||
color: var(--text);
|
||
}
|
||
</style>
|
||
<!-- Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<!-- Highlight.js -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||
<!-- Marked.js -->
|
||
<script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js"></script>
|
||
<!-- Lucide Icons -->
|
||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||
|
||
<style>
|
||
:root {
|
||
/* SaaS Color Palette */
|
||
--primary: #2563EB;
|
||
--primary-hover: #1D4ED8;
|
||
--secondary: #3B82F6;
|
||
--cta: #F97316;
|
||
--cta-hover: #EA580C;
|
||
--background: #F8FAFC;
|
||
--surface: #FFFFFF;
|
||
--text: #1E293B;
|
||
--text-muted: #64748B;
|
||
--border: #E2E8F0;
|
||
--success: #10B981;
|
||
--warning: #F59E0B;
|
||
--error: #EF4444;
|
||
|
||
/* Glass effect */
|
||
--glass-bg: rgba(255, 255, 255, 0.8);
|
||
--glass-border: rgba(226, 232, 240, 0.8);
|
||
--glass-blur: 12px;
|
||
|
||
/* Spacing */
|
||
--sidebar-width: 280px;
|
||
--header-height: 64px;
|
||
}
|
||
|
||
.dark {
|
||
--primary: #3B82F6;
|
||
--primary-hover: #60A5FA;
|
||
--secondary: #60A5FA;
|
||
--cta: #FB923C;
|
||
--cta-hover: #F97316;
|
||
--background: #0F172A;
|
||
--surface: #1E293B;
|
||
--text: #F1F5F9;
|
||
--text-muted: #94A3B8;
|
||
--border: #334155;
|
||
--glass-bg: rgba(30, 41, 59, 0.8);
|
||
--glass-border: rgba(51, 65, 85, 0.8);
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Open Sans', sans-serif;
|
||
background: var(--background);
|
||
color: var(--text);
|
||
height: 100vh;
|
||
display: flex;
|
||
overflow: hidden;
|
||
transition: background-color 0.3s ease, color 0.3s ease;
|
||
}
|
||
|
||
/* ===== Sidebar ===== */
|
||
.sidebar {
|
||
width: var(--sidebar-width);
|
||
background: var(--surface);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||
z-index: 20;
|
||
}
|
||
|
||
.sidebar.collapsed {
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.sidebar-header h1 {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--primary);
|
||
}
|
||
|
||
.new-chat-btn {
|
||
width: calc(100% - 32px);
|
||
margin: 16px 16px;
|
||
padding: 12px 16px;
|
||
background: var(--primary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.new-chat-btn:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
.chat-history {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 0 12px;
|
||
}
|
||
|
||
.chat-history-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.chat-history-title {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
color: var(--text-muted);
|
||
padding: 8px 12px;
|
||
}
|
||
|
||
.chat-history-item {
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
transition: background-color 0.15s ease;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.chat-history-item:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.chat-history-item.active {
|
||
background: rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.dark .chat-history-item.active {
|
||
background: rgba(59, 130, 246, 0.2);
|
||
}
|
||
|
||
.chat-history-item-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.chat-history-item-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.chat-history-item-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.chat-history-item-preview {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.chat-history-item-delete {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
opacity: 0;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.chat-history-item:hover .chat-history-item-delete {
|
||
opacity: 1;
|
||
}
|
||
|
||
.chat-history-item-delete:hover {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
color: var(--error);
|
||
}
|
||
|
||
.sidebar-footer {
|
||
padding: 12px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.sidebar-footer-btn {
|
||
flex: 1;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: var(--text);
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.sidebar-footer-btn:hover {
|
||
background: var(--background);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
/* ===== Main Content ===== */
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* ===== Header ===== */
|
||
.header {
|
||
height: var(--header-height);
|
||
background: var(--glass-bg);
|
||
backdrop-filter: blur(var(--glass-blur));
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 20px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.sidebar-toggle {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 8px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: background-color 0.15s ease;
|
||
}
|
||
|
||
.sidebar-toggle:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.header-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header-center {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.status-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
background: var(--background);
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--success);
|
||
}
|
||
|
||
.status-dot.connecting {
|
||
background: var(--warning);
|
||
}
|
||
|
||
.status-dot.error {
|
||
background: var(--error);
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.header-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 8px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.header-btn:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.header-btn.primary {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.header-btn.primary:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
/* ===== Chat Messages ===== */
|
||
.chat-container {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 24px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.welcome-screen {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.welcome-logo {
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 20px;
|
||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.welcome-logo svg {
|
||
width: 40px;
|
||
height: 40px;
|
||
color: white;
|
||
}
|
||
|
||
.welcome-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.welcome-subtitle {
|
||
font-size: 15px;
|
||
color: var(--text-muted);
|
||
max-width: 400px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.suggestion-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
max-width: 600px;
|
||
}
|
||
|
||
.suggestion-chip {
|
||
padding: 10px 16px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.suggestion-chip:hover {
|
||
border-color: var(--primary);
|
||
color: var(--primary);
|
||
}
|
||
|
||
/* ===== Messages ===== */
|
||
.message {
|
||
display: flex;
|
||
gap: 12px;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
}
|
||
|
||
.message.user {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
.message-avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.message.assistant .message-avatar {
|
||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||
color: white;
|
||
}
|
||
|
||
.message.user .message-avatar {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.message-content {
|
||
padding: 14px 18px;
|
||
border-radius: 16px;
|
||
max-width: calc(100% - 48px);
|
||
line-height: 1.6;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.message.assistant .message-content {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
|
||
.message.user .message-content {
|
||
background: var(--primary);
|
||
color: white;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
|
||
/* ===== Markdown Styles ===== */
|
||
.message-content h1,
|
||
.message-content h2,
|
||
.message-content h3,
|
||
.message-content h4,
|
||
.message-content h5,
|
||
.message-content h6 {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-weight: 600;
|
||
margin: 16px 0 8px;
|
||
}
|
||
|
||
.message-content h1 { font-size: 1.4em; }
|
||
.message-content h2 { font-size: 1.3em; }
|
||
.message-content h3 { font-size: 1.2em; }
|
||
|
||
.message-content p {
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.message-content code {
|
||
background: rgba(0, 0, 0, 0.05);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-family: 'Monaco', 'Consolas', monospace;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.dark .message-content code {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.message.user .message-content code {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
}
|
||
|
||
.message-content pre {
|
||
background: var(--background);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 14px;
|
||
overflow-x: auto;
|
||
margin: 12px 0;
|
||
}
|
||
|
||
.message-content pre code {
|
||
background: transparent;
|
||
padding: 0;
|
||
}
|
||
|
||
.message-content ul,
|
||
.message-content ol {
|
||
margin: 8px 0;
|
||
padding-left: 24px;
|
||
}
|
||
|
||
.message-content li {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.message-content blockquote {
|
||
border-left: 3px solid var(--primary);
|
||
padding-left: 14px;
|
||
margin: 12px 0;
|
||
color: var(--text-muted);
|
||
font-style: italic;
|
||
}
|
||
|
||
.message-content a {
|
||
color: var(--primary);
|
||
}
|
||
|
||
.message.user .message-content a {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
/* ===== Tool Sections ===== */
|
||
.tool-section {
|
||
margin: 12px 0;
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
background: var(--background);
|
||
}
|
||
|
||
.tool-header {
|
||
padding: 10px 14px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
transition: background-color 0.15s ease;
|
||
}
|
||
|
||
.tool-header:hover {
|
||
background: var(--border);
|
||
}
|
||
|
||
.tool-header .tool-icon {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.tool-header.collapsed .tool-icon {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.tool-call .tool-header {
|
||
background: rgba(37, 99, 235, 0.08);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.tool-response .tool-header {
|
||
background: rgba(16, 185, 129, 0.08);
|
||
color: var(--success);
|
||
}
|
||
|
||
.tool-content {
|
||
padding: 14px;
|
||
font-family: 'Monaco', 'Consolas', monospace;
|
||
font-size: 12px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
white-space: pre-wrap;
|
||
background: var(--surface);
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.tool-content.collapsed {
|
||
display: none;
|
||
}
|
||
|
||
/* ===== Typing Indicator ===== */
|
||
.typing-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 14px 18px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
border-bottom-left-radius: 4px;
|
||
font-size: 13px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.typing-dots {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.typing-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--text-muted);
|
||
animation: typing 1.4s infinite ease-in-out;
|
||
}
|
||
|
||
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
|
||
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
|
||
|
||
@keyframes typing {
|
||
0%, 80%, 100% {
|
||
transform: scale(0.8);
|
||
opacity: 0.5;
|
||
}
|
||
40% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* ===== Input Area ===== */
|
||
.input-container {
|
||
padding: 16px 24px 24px;
|
||
background: var(--glass-bg);
|
||
backdrop-filter: blur(var(--glass-blur));
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.input-wrapper {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.input-box {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 12px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 8px 14px;
|
||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||
}
|
||
|
||
.input-box:focus-within {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.dark .input-box:focus-within {
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||
}
|
||
|
||
.chat-input {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
outline: none;
|
||
font-size: 14px;
|
||
resize: none;
|
||
max-height: 150px;
|
||
min-height: 24px;
|
||
line-height: 1.5;
|
||
font-family: 'Open Sans', sans-serif;
|
||
}
|
||
|
||
.chat-input::placeholder {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.input-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.input-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.input-btn:hover {
|
||
background: var(--background);
|
||
color: var(--text);
|
||
}
|
||
|
||
.input-btn.send {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.input-btn.send:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
.input-btn.send:disabled {
|
||
background: var(--border);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* ===== Settings Modal ===== */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(4px);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
}
|
||
|
||
.modal-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal {
|
||
background: var(--surface);
|
||
border-radius: 16px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
max-height: 85vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.modal-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
background: var(--background);
|
||
color: var(--text);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 24px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.settings-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.settings-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.settings-group.full-width {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.settings-label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.settings-input,
|
||
.settings-select {
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
background: var(--background);
|
||
color: var(--text);
|
||
outline: none;
|
||
transition: border-color 0.15s ease;
|
||
}
|
||
|
||
.settings-input:focus,
|
||
.settings-select:focus {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.settings-textarea {
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
background: var(--background);
|
||
color: var(--text);
|
||
outline: none;
|
||
resize: vertical;
|
||
min-height: 80px;
|
||
font-family: 'Open Sans', sans-serif;
|
||
}
|
||
|
||
.settings-checkbox-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.settings-checkbox {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 4px;
|
||
border: 1px solid var(--border);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 16px 24px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--background);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
border: none;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
/* ===== Error Message ===== */
|
||
.error-banner {
|
||
padding: 12px 16px;
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
border-radius: 10px;
|
||
color: var(--error);
|
||
font-size: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* ===== Scrollbar ===== */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: var(--text-muted);
|
||
}
|
||
|
||
/* ===== Responsive ===== */
|
||
@media (max-width: 1024px) {
|
||
.sidebar {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.sidebar.open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.sidebar-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 15;
|
||
display: none;
|
||
}
|
||
|
||
.sidebar-overlay.active {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.header {
|
||
padding: 0 12px;
|
||
}
|
||
|
||
.header-center {
|
||
display: none;
|
||
}
|
||
|
||
.chat-container {
|
||
padding: 16px;
|
||
}
|
||
|
||
.input-container {
|
||
padding: 12px 16px 16px;
|
||
}
|
||
|
||
.settings-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.modal {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
height: 100%;
|
||
max-height: 100%;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.welcome-title {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.suggestion-chips {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.suggestion-chip {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* ===== Animation ===== */
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(8px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.message {
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
/* ===== Skills Grid ===== */
|
||
.skills-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||
gap: 10px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
background: var(--background);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.skill-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.skill-item:hover {
|
||
border-color: var(--primary);
|
||
background: rgba(37, 99, 235, 0.05);
|
||
}
|
||
|
||
.skill-item.selected {
|
||
border-color: var(--primary);
|
||
background: rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.skill-item input[type="checkbox"] {
|
||
width: 16px;
|
||
height: 16px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.skill-item-label {
|
||
flex: 1;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.skill-item-desc {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.skills-loading {
|
||
grid-column: 1 / -1;
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* ===== MCP Grid ===== */
|
||
.mcp-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
background: var(--background);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.mcp-item {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.mcp-item.enabled {
|
||
border-color: var(--success);
|
||
}
|
||
|
||
.mcp-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.mcp-header:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.mcp-checkbox {
|
||
width: 18px;
|
||
height: 18px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.mcp-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.mcp-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.mcp-desc {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.mcp-toggle {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.mcp-item.expanded .mcp-toggle {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.mcp-config {
|
||
padding: 0 12px 12px;
|
||
display: none;
|
||
border-top: 1px solid var(--border);
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.mcp-item.expanded .mcp-config {
|
||
display: block;
|
||
}
|
||
|
||
.mcp-config-item {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.mcp-config-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.mcp-config-label {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.mcp-config-input {
|
||
width: 100%;
|
||
padding: 8px 10px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
background: var(--background);
|
||
color: var(--text);
|
||
outline: none;
|
||
}
|
||
|
||
.mcp-config-input:focus {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
/* ===== Settings Tabs ===== */
|
||
.settings-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 0 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.settings-tab {
|
||
padding: 12px 16px;
|
||
background: transparent;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
color: var(--text-muted);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.settings-tab:hover {
|
||
color: var(--text);
|
||
}
|
||
|
||
.settings-tab.active {
|
||
color: var(--primary);
|
||
border-bottom-color: var(--primary);
|
||
}
|
||
|
||
.settings-tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.settings-tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
/* ===== Large Modal ===== */
|
||
.settings-modal-large {
|
||
max-width: 700px;
|
||
max-height: 85vh;
|
||
}
|
||
|
||
/* ===== Skills Grid Large ===== */
|
||
.skills-grid-large {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
gap: 12px;
|
||
max-height: 350px;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
background: var(--background);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
/* ===== MCP Manager Button ===== */
|
||
.mcp-manager-btn {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
background: var(--background);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.mcp-manager-btn:hover {
|
||
border-color: var(--primary);
|
||
background: rgba(37, 99, 235, 0.05);
|
||
}
|
||
|
||
/* ===== MCP Manager Modal ===== */
|
||
.mcp-manager-modal {
|
||
max-width: 600px;
|
||
max-height: 80vh;
|
||
}
|
||
|
||
.mcp-manager-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.mcp-manager-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
background: var(--background);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.mcp-manager-item:hover {
|
||
border-color: var(--border);
|
||
background: var(--surface);
|
||
}
|
||
|
||
.mcp-manager-item.enabled {
|
||
border-color: var(--success);
|
||
background: rgba(16, 185, 129, 0.05);
|
||
}
|
||
|
||
.mcp-manager-checkbox {
|
||
width: 20px;
|
||
height: 20px;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mcp-manager-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.mcp-manager-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--text);
|
||
}
|
||
|
||
.mcp-manager-type {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
font-family: 'Monaco', 'Consolas', monospace;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.mcp-manager-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.mcp-manager-action-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 6px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.mcp-manager-action-btn:hover {
|
||
background: var(--background);
|
||
color: var(--text);
|
||
}
|
||
|
||
.mcp-manager-action-btn.edit:hover {
|
||
background: rgba(37, 99, 235, 0.1);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.mcp-manager-action-btn.delete:hover {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
color: var(--error);
|
||
}
|
||
|
||
.btn-add-mcp {
|
||
padding: 8px 16px;
|
||
background: var(--primary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.btn-add-mcp:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
/* ===== Empty State ===== */
|
||
.mcp-empty-state {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.mcp-empty-state-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
margin: 0 auto 16px;
|
||
border-radius: 12px;
|
||
background: var(--background);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* ===== MCP JSON Editor ===== */
|
||
.mcp-json-editor {
|
||
min-height: 150px !important;
|
||
font-family: 'Monaco', 'Consolas', 'Menlo', monospace !important;
|
||
font-size: 13px !important;
|
||
line-height: 1.5 !important;
|
||
tab-size: 2;
|
||
}
|
||
|
||
.mcp-json-hint {
|
||
padding: 10px 12px;
|
||
background: var(--background);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.mcp-manager-config {
|
||
font-family: 'Monaco', 'Consolas', monospace;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 2px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Sidebar Overlay (Mobile) -->
|
||
<div class="sidebar-overlay" id="sidebar-overlay"></div>
|
||
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<h1>AI Assistant</h1>
|
||
<button class="header-btn" id="sidebar-close-btn" style="display: none;">
|
||
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<button class="new-chat-btn" id="new-chat-btn">
|
||
<i data-lucide="plus" style="width: 16px; height: 16px;"></i>
|
||
新对话
|
||
</button>
|
||
|
||
<div class="chat-history" id="chat-history">
|
||
<!-- Chat history will be dynamically rendered -->
|
||
</div>
|
||
|
||
<div class="sidebar-footer">
|
||
<button class="sidebar-footer-btn" id="settings-btn">
|
||
<i data-lucide="settings" style="width: 16px; height: 16px;"></i>
|
||
设置
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main Content -->
|
||
<main class="main-content">
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<div class="header-left">
|
||
<button class="sidebar-toggle" id="sidebar-toggle">
|
||
<i data-lucide="menu" style="width: 20px; height: 20px;"></i>
|
||
</button>
|
||
<!-- Bot Selector -->
|
||
<div class="bot-selector" id="bot-selector">
|
||
<div class="bot-selector-trigger" id="bot-selector-trigger">
|
||
<span class="bot-selector-current" id="bot-selector-current">选择 Bot...</span>
|
||
<i data-lucide="chevron-down" style="width: 16px; height: 16px;"></i>
|
||
</div>
|
||
<div class="bot-selector-dropdown" id="bot-selector-dropdown">
|
||
<!-- 动态生成 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header-center">
|
||
<div class="status-badge">
|
||
<span class="status-dot" id="status-dot"></span>
|
||
<span id="status-text">已连接</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="header-right">
|
||
<button class="header-btn" id="theme-toggle" title="切换主题">
|
||
<i data-lucide="moon" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
<button class="header-btn primary" id="header-settings-btn">
|
||
<i data-lucide="settings" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Bot Selection Modal -->
|
||
<div class="bot-selection-modal hidden" id="bot-selection-modal">
|
||
<div class="bot-selection-content">
|
||
<div class="bot-selection-icon">
|
||
<i data-lucide="bot" style="width: 32px; height: 32px;"></i>
|
||
</div>
|
||
<h2 class="bot-selection-title">请选择一个 Bot</h2>
|
||
<p class="bot-selection-text">您需要先选择一个 Bot 才能开始聊天</p>
|
||
<div class="bot-selection-actions">
|
||
<button class="btn btn-secondary" id="go-to-manager">管理 Bots</button>
|
||
<button class="btn btn-primary" id="select-existing-bot">选择 Bot</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Container -->
|
||
<div class="chat-container" id="chat-container">
|
||
<!-- Welcome Screen (shown when no messages) -->
|
||
<div class="welcome-screen" id="welcome-screen">
|
||
<div class="welcome-logo">
|
||
<i data-lucide="sparkles" style="width: 40px; height: 40px;"></i>
|
||
</div>
|
||
<h2 class="welcome-title">AI 聊天助手</h2>
|
||
<p class="welcome-subtitle">有什么可以帮助您的吗?我可以回答问题、编写代码、提供创意等。</p>
|
||
<div class="suggestion-chips">
|
||
<button class="suggestion-chip">用 Python 写一个排序算法</button>
|
||
<button class="suggestion-chip">解释量子计算的原理</button>
|
||
<button class="suggestion-chip">帮我写一首关于秋天的诗</button>
|
||
<button class="suggestion-chip">推荐一些学习资源</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Input Area -->
|
||
<div class="input-container">
|
||
<div class="input-wrapper">
|
||
<div class="input-box">
|
||
<textarea
|
||
class="chat-input"
|
||
id="chat-input"
|
||
placeholder="输入您的问题..."
|
||
rows="1"
|
||
></textarea>
|
||
<div class="input-actions">
|
||
<button class="input-btn" id="attach-btn" title="附加文件">
|
||
<i data-lucide="paperclip" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
<button class="input-btn send" id="send-btn">
|
||
<i data-lucide="send" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Settings Modal -->
|
||
<div class="modal-overlay" id="settings-modal">
|
||
<div class="modal settings-modal-large">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title">设置</h3>
|
||
<button class="modal-close" id="modal-close">
|
||
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
</div>
|
||
<!-- Tabs -->
|
||
<div class="settings-tabs">
|
||
<button class="settings-tab active" data-tab="basic">基础设置</button>
|
||
<button class="settings-tab" data-tab="prompt">系统提示词</button>
|
||
<button class="settings-tab" data-tab="advanced">高级配置</button>
|
||
<button class="settings-tab" data-tab="skills">MCP & Skill</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<!-- 基础设置 Tab -->
|
||
<div class="settings-tab-content active" data-tab="basic">
|
||
<div class="settings-grid">
|
||
<div class="settings-group">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
||
<label class="settings-label" for="model-select" style="margin-bottom: 0;">模型</label>
|
||
<a href="model-manager.html" style="font-size: 12px; color: var(--primary); text-decoration: none; display: flex; align-items: center; gap: 4px;">
|
||
<i data-lucide="settings" style="width: 12px; height: 12px;"></i>
|
||
管理模型
|
||
</a>
|
||
</div>
|
||
<select id="model-select" class="settings-select">
|
||
<option value="">选择模型...</option>
|
||
<!-- 动态生成 -->
|
||
</select>
|
||
<input type="hidden" id="model" value="">
|
||
<input type="hidden" id="api-key" value="">
|
||
<input type="hidden" id="model-server" value="">
|
||
</div>
|
||
<div class="settings-group" style="display: none;" id="model-detail-group">
|
||
<label class="settings-label">模型详情</label>
|
||
<div style="font-size: 12px; color: var(--text-muted); padding: 10px; background: var(--background); border-radius: 8px; border: 1px solid var(--border);">
|
||
<div style="margin-bottom: 4px;"><span style="color: var(--text-muted);">服务器:</span><span id="detail-server">-</span></div>
|
||
<div style="margin-bottom: 4px;"><span style="color: var(--text-muted);">API Key:</span><span id="detail-apikey">-</span></div>
|
||
<div><span style="color: var(--text-muted);">提供商:</span><span id="detail-provider">-</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="settings-group">
|
||
<label class="settings-label" for="api-domain">API域名</label>
|
||
<select id="api-domain" class="settings-select">
|
||
<option value="current">当前域名</option>
|
||
<option value="https://catalog-agent.gbase.ai">https://catalog-agent.gbase.ai</option>
|
||
<option value="https://catalog-agent-dev.gbase.ai">https://catalog-agent-dev.gbase.ai</option>
|
||
<option value="http://localhost:8001">http://localhost:8001</option>
|
||
<option value="http://qwen-agent-api:8001">http://qwen-agent-api:8001</option>
|
||
<option value="custom">自定义</option>
|
||
</select>
|
||
</div>
|
||
<div class="settings-group full-width" id="custom-api-domain-group" style="display: none;">
|
||
<label class="settings-label" for="custom-api-domain">自定义API域名</label>
|
||
<input type="text" id="custom-api-domain" class="settings-input" placeholder="https://your-api-domain.com">
|
||
</div>
|
||
<div class="settings-group">
|
||
<label class="settings-label" for="language">语言</label>
|
||
<select id="language" class="settings-select">
|
||
<option value="zh-CN" selected>中文</option>
|
||
<option value="en">English</option>
|
||
<option value="ja">日本語</option>
|
||
<option value="ko">한국어</option>
|
||
</select>
|
||
</div>
|
||
<div class="settings-group">
|
||
<label class="settings-label" for="robot-type">机器人类型</label>
|
||
<select id="robot-type" class="settings-select">
|
||
<option value="">默认</option>
|
||
<option value="agent">Agent</option>
|
||
<option value="catalog_agent">Catalog Agent</option>
|
||
<option value="deep_agent">Deep Agent</option>
|
||
</select>
|
||
</div>
|
||
<div class="settings-group">
|
||
<div class="settings-checkbox-wrapper">
|
||
<input type="checkbox" id="tool-response" class="settings-checkbox">
|
||
<label class="settings-label" for="tool-response" style="margin-bottom: 0;">启用工具响应</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统提示词 Tab -->
|
||
<div class="settings-tab-content" data-tab="prompt">
|
||
<div class="settings-group full-width">
|
||
<label class="settings-label" for="system-prompt">系统提示词</label>
|
||
<textarea id="system-prompt" class="settings-textarea" placeholder="输入系统提示词..." style="min-height: 200px;"></textarea>
|
||
<p style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
|
||
系统提示词用于定义 AI 助手的行为和角色。您可以在这里设置助手的个性、知识范围和回答风格。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 高级配置 Tab -->
|
||
<div class="settings-tab-content" data-tab="advanced">
|
||
<div class="settings-grid">
|
||
<div class="settings-group full-width">
|
||
<label class="settings-label" for="dataset-ids">数据集ID (逗号分隔)</label>
|
||
<input type="text" id="dataset-ids" class="settings-input" placeholder="例如: dataset1,dataset2">
|
||
</div>
|
||
<div class="settings-group">
|
||
<label class="settings-label" for="user-identifier">用户标识</label>
|
||
<input type="text" id="user-identifier" class="settings-input" placeholder="输入用户标识...">
|
||
</div>
|
||
<div class="settings-group">
|
||
<div class="settings-checkbox-wrapper">
|
||
<input type="checkbox" id="enable-memori" class="settings-checkbox">
|
||
<label class="settings-label" for="enable-memori" style="margin-bottom: 0;">启用记忆存储</label>
|
||
</div>
|
||
<p style="font-size: 11px; color: var(--text-muted); margin-top: 4px;">启用后,AI 会记住对话中的信息以提供更个性化的回复</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MCP & Skill Tab -->
|
||
<div class="settings-tab-content" data-tab="skills">
|
||
<!-- MCP 服务器管理 -->
|
||
<div class="settings-group full-width" style="margin-bottom: 24px;">
|
||
<label class="settings-label">MCP 服务器</label>
|
||
<button type="button" id="open-mcp-manager-btn" class="mcp-manager-btn">
|
||
<i data-lucide="settings" style="width: 16px; height: 16px;"></i>
|
||
管理 MCP 服务器 (<span id="mcp-count">0</span> 个已启用)
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 技能选择 -->
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
||
<label class="settings-label">可用技能</label>
|
||
<button type="button" id="refresh-skills-btn" style="background: none; border: none; color: var(--primary); cursor: pointer; font-size: 12px; padding: 4px 8px; border-radius: 4px;">
|
||
<i data-lucide="refresh-cw" style="width: 14px; height: 14px;"></i>
|
||
刷新
|
||
</button>
|
||
</div>
|
||
<div id="skills-container" class="skills-grid-large">
|
||
<div class="skills-loading">加载中...</div>
|
||
</div>
|
||
<input type="hidden" id="skills" value="">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-right: auto;">
|
||
<button class="btn btn-secondary" id="import-settings-btn" style="padding: 8px 16px; font-size: 13px;">
|
||
<i data-lucide="upload" style="width: 14px; height: 14px; margin-right: 4px;"></i>
|
||
导入
|
||
</button>
|
||
<button class="btn btn-secondary" id="export-settings-btn" style="padding: 8px 16px; font-size: 13px;">
|
||
<i data-lucide="download" style="width: 14px; height: 14px; margin-right: 4px;"></i>
|
||
导出
|
||
</button>
|
||
</div>
|
||
<button class="btn btn-secondary" id="modal-cancel">取消</button>
|
||
<button class="btn btn-primary" id="modal-save">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 隐藏的文件输入用于导入 -->
|
||
<input type="file" id="import-file-input" accept=".json" style="display: none;">
|
||
|
||
<!-- MCP Manager Modal -->
|
||
<div class="modal-overlay" id="mcp-manager-modal">
|
||
<div class="modal mcp-manager-modal">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title">MCP 服务器管理</h3>
|
||
<button class="modal-close" id="mcp-manager-close">
|
||
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<!-- 添加新服务器按钮 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||
<span class="settings-label">已配置的服务器</span>
|
||
<button type="button" id="add-mcp-server-btn" class="btn-add-mcp">
|
||
<i data-lucide="plus" style="width: 14px; height: 14px;"></i>
|
||
添加服务器
|
||
</button>
|
||
</div>
|
||
<!-- MCP 服务器列表 -->
|
||
<div id="mcp-manager-list" class="mcp-manager-list">
|
||
<!-- 动态生成 -->
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-primary" id="mcp-manager-done">完成</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MCP Server Edit Modal -->
|
||
<div class="modal-overlay" id="mcp-edit-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title" id="mcp-edit-title">添加 MCP 服务器</h3>
|
||
<button class="modal-close" id="mcp-edit-close">
|
||
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="settings-group full-width">
|
||
<label class="settings-label" for="mcp-edit-json">MCP 配置 (JSON)</label>
|
||
<textarea id="mcp-edit-json" class="settings-textarea mcp-json-editor" placeholder='输入 MCP 服务器配置,例如: { "服务器名称": { "url": "http://example.com/sse", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"] } } 根据服务器类型填入所需字段即可' style="min-height: 200px; font-family: 'Monaco', 'Consolas', monospace; font-size: 13px;"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" id="mcp-edit-cancel">取消</button>
|
||
<button class="btn btn-primary" id="mcp-edit-save">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Initialize Lucide icons
|
||
lucide.createIcons();
|
||
|
||
// ===== Bot Manager =====
|
||
class BotManager {
|
||
constructor() {
|
||
this.bots = [];
|
||
this.currentBot = null;
|
||
this.loadBots();
|
||
}
|
||
|
||
loadBots() {
|
||
const stored = localStorage.getItem('bot-list');
|
||
if (stored) {
|
||
try {
|
||
this.bots = JSON.parse(stored);
|
||
} catch (e) {
|
||
this.bots = [];
|
||
}
|
||
}
|
||
}
|
||
|
||
saveBots() {
|
||
localStorage.setItem('bot-list', JSON.stringify(this.bots));
|
||
}
|
||
|
||
getBotById(internalId) {
|
||
return this.bots.find(b => b.id === internalId);
|
||
}
|
||
|
||
setCurrentBot(internalId) {
|
||
this.currentBot = this.getBotById(internalId);
|
||
if (this.currentBot) {
|
||
sessionStorage.setItem('current-bot-id', internalId);
|
||
}
|
||
return this.currentBot;
|
||
}
|
||
|
||
getCurrentBot() {
|
||
if (!this.currentBot) {
|
||
const savedId = sessionStorage.getItem('current-bot-id');
|
||
if (savedId) {
|
||
this.currentBot = this.getBotById(savedId);
|
||
}
|
||
}
|
||
return this.currentBot;
|
||
}
|
||
|
||
getBotSettingsKey() {
|
||
const bot = this.getCurrentBot();
|
||
return bot ? `bot-settings-${bot.botId}` : null;
|
||
}
|
||
}
|
||
|
||
// Initialize Bot Manager
|
||
const botManager = new BotManager();
|
||
|
||
class ChatApp {
|
||
constructor() {
|
||
this.elements = {
|
||
sidebar: document.getElementById('sidebar'),
|
||
sidebarToggle: document.getElementById('sidebar-toggle'),
|
||
sidebarCloseBtn: document.getElementById('sidebar-close-btn'),
|
||
sidebarOverlay: document.getElementById('sidebar-overlay'),
|
||
themeToggle: document.getElementById('theme-toggle'),
|
||
settingsBtn: document.getElementById('settings-btn'),
|
||
headerSettingsBtn: document.getElementById('header-settings-btn'),
|
||
settingsModal: document.getElementById('settings-modal'),
|
||
modalClose: document.getElementById('modal-close'),
|
||
modalCancel: document.getElementById('modal-cancel'),
|
||
modalSave: document.getElementById('modal-save'),
|
||
chatContainer: document.getElementById('chat-container'),
|
||
chatInput: document.getElementById('chat-input'),
|
||
sendBtn: document.getElementById('send-btn'),
|
||
welcomeScreen: document.getElementById('welcome-screen'),
|
||
statusDot: document.getElementById('status-dot'),
|
||
statusText: document.getElementById('status-text'),
|
||
newChatBtn: document.getElementById('new-chat-btn'),
|
||
apiDomain: document.getElementById('api-domain'),
|
||
customApiDomainGroup: document.getElementById('custom-api-domain-group'),
|
||
skillsContainer: document.getElementById('skills-container'),
|
||
refreshSkillsBtn: document.getElementById('refresh-skills-btn'),
|
||
// Bot Selector
|
||
botSelector: document.getElementById('bot-selector'),
|
||
botSelectorTrigger: document.getElementById('bot-selector-trigger'),
|
||
botSelectorCurrent: document.getElementById('bot-selector-current'),
|
||
botSelectorDropdown: document.getElementById('bot-selector-dropdown'),
|
||
// Bot Selection Modal
|
||
botSelectionModal: document.getElementById('bot-selection-modal'),
|
||
goToManager: document.getElementById('go-to-manager'),
|
||
selectExistingBot: document.getElementById('select-existing-bot'),
|
||
// MCP Manager
|
||
mcpManagerBtn: document.getElementById('open-mcp-manager-btn'),
|
||
mcpManagerModal: document.getElementById('mcp-manager-modal'),
|
||
mcpManagerClose: document.getElementById('mcp-manager-close'),
|
||
mcpManagerDone: document.getElementById('mcp-manager-done'),
|
||
mcpManagerList: document.getElementById('mcp-manager-list'),
|
||
addMcpServerBtn: document.getElementById('add-mcp-server-btn'),
|
||
// MCP Edit
|
||
mcpEditModal: document.getElementById('mcp-edit-modal'),
|
||
mcpEditClose: document.getElementById('mcp-edit-close'),
|
||
mcpEditCancel: document.getElementById('mcp-edit-cancel'),
|
||
mcpEditSave: document.getElementById('mcp-edit-save'),
|
||
mcpEditJson: document.getElementById('mcp-edit-json'),
|
||
mcpEditTitle: document.getElementById('mcp-edit-title')
|
||
};
|
||
|
||
this.currentBot = null;
|
||
|
||
// 从 localStorage 加载自定义 MCP 服务器
|
||
this.customMcpServers = this.loadCustomMcpServers();
|
||
|
||
// 当前编辑的 MCP 服务器索引
|
||
this.editingMcpIndex = -1;
|
||
|
||
this.isProcessing = false;
|
||
this.messages = [];
|
||
this.currentController = null;
|
||
this.currentSessionId = null;
|
||
this.allSessions = [];
|
||
|
||
this.initializeEventListeners();
|
||
this.initializeSettingsTabs();
|
||
this.loadSettings();
|
||
this.loadTheme();
|
||
this.loadChatHistory();
|
||
}
|
||
|
||
initializeEventListeners() {
|
||
// Bot selector
|
||
this.elements.botSelectorTrigger.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this.toggleBotDropdown();
|
||
});
|
||
|
||
document.addEventListener('click', () => {
|
||
this.elements.botSelectorDropdown.classList.remove('show');
|
||
this.elements.botSelectorTrigger.classList.remove('active');
|
||
});
|
||
|
||
this.elements.botSelectorDropdown.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
});
|
||
|
||
this.elements.goToManager.addEventListener('click', () => {
|
||
window.location.href = 'bot-manager.html';
|
||
});
|
||
|
||
this.elements.selectExistingBot.addEventListener('click', () => {
|
||
this.closeBotSelectionModal();
|
||
this.toggleBotDropdown();
|
||
});
|
||
|
||
// Sidebar toggle
|
||
this.elements.sidebarToggle.addEventListener('click', () => this.toggleSidebar());
|
||
this.elements.sidebarCloseBtn.addEventListener('click', () => this.closeSidebar());
|
||
this.elements.sidebarOverlay.addEventListener('click', () => this.closeSidebar());
|
||
|
||
// Theme toggle
|
||
this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
|
||
|
||
// Settings modal
|
||
this.elements.settingsBtn.addEventListener('click', () => this.openSettings());
|
||
this.elements.headerSettingsBtn.addEventListener('click', () => this.openSettings());
|
||
this.elements.modalClose.addEventListener('click', () => this.closeSettings());
|
||
this.elements.modalCancel.addEventListener('click', () => this.closeSettings());
|
||
this.elements.modalSave.addEventListener('click', () => this.saveAndCloseSettings());
|
||
this.elements.settingsModal.addEventListener('click', (e) => {
|
||
if (e.target === this.elements.settingsModal) this.closeSettings();
|
||
});
|
||
|
||
// Chat input
|
||
this.elements.chatInput.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
this.sendMessage();
|
||
}
|
||
});
|
||
this.elements.sendBtn.addEventListener('click', () => this.sendMessage());
|
||
|
||
// Auto-resize textarea
|
||
this.elements.chatInput.addEventListener('input', () => {
|
||
this.elements.chatInput.style.height = 'auto';
|
||
this.elements.chatInput.style.height = Math.min(this.elements.chatInput.scrollHeight, 150) + 'px';
|
||
});
|
||
|
||
// API domain change
|
||
this.elements.apiDomain.addEventListener('change', () => {
|
||
this.elements.customApiDomainGroup.style.display =
|
||
this.elements.apiDomain.value === 'custom' ? 'block' : 'none';
|
||
this.saveSettings();
|
||
});
|
||
|
||
// New chat
|
||
this.elements.newChatBtn.addEventListener('click', () => this.startNewChat());
|
||
|
||
// Refresh skills
|
||
this.elements.refreshSkillsBtn.addEventListener('click', () => this.loadSkills());
|
||
|
||
// MCP Manager
|
||
this.elements.mcpManagerBtn.addEventListener('click', () => this.openMCPManager());
|
||
this.elements.mcpManagerClose.addEventListener('click', () => this.closeMCPManager());
|
||
this.elements.mcpManagerDone.addEventListener('click', () => this.closeMCPManager());
|
||
this.elements.mcpManagerModal.addEventListener('click', (e) => {
|
||
if (e.target === this.elements.mcpManagerModal) this.closeMCPManager();
|
||
});
|
||
|
||
// Add MCP Server
|
||
this.elements.addMcpServerBtn.addEventListener('click', () => this.openMcpEditModal());
|
||
|
||
// MCP Edit Modal
|
||
this.elements.mcpEditClose.addEventListener('click', () => this.closeMcpEditModal());
|
||
this.elements.mcpEditCancel.addEventListener('click', () => this.closeMcpEditModal());
|
||
this.elements.mcpEditSave.addEventListener('click', () => this.saveMcpEdit());
|
||
this.elements.mcpEditModal.addEventListener('click', (e) => {
|
||
if (e.target === this.elements.mcpEditModal) this.closeMcpEditModal();
|
||
});
|
||
|
||
// Import/Export Settings
|
||
document.getElementById('export-settings-btn').addEventListener('click', () => this.exportSettings());
|
||
document.getElementById('import-settings-btn').addEventListener('click', () => {
|
||
document.getElementById('import-file-input').click();
|
||
});
|
||
document.getElementById('import-file-input').addEventListener('change', (e) => this.importSettings(e));
|
||
|
||
// Suggestion chips
|
||
document.querySelectorAll('.suggestion-chip').forEach(chip => {
|
||
chip.addEventListener('click', () => {
|
||
this.elements.chatInput.value = chip.textContent;
|
||
this.sendMessage();
|
||
});
|
||
});
|
||
|
||
// Settings auto-save on change
|
||
document.querySelectorAll('.settings-input, .settings-select, .settings-textarea, .settings-checkbox').forEach(input => {
|
||
input.addEventListener('change', () => this.saveSettings());
|
||
});
|
||
}
|
||
|
||
initializeSettingsTabs() {
|
||
// 初始化 bot 选择器
|
||
this.initializeBotSelector();
|
||
// 初始化模型选择器
|
||
this.initializeModelSelector();
|
||
|
||
// 标签页切换
|
||
document.querySelectorAll('.settings-tab').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const targetTab = tab.dataset.tab;
|
||
// 切换标签状态
|
||
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
// 切换内容
|
||
document.querySelectorAll('.settings-tab-content').forEach(content => {
|
||
content.classList.remove('active');
|
||
if (content.dataset.tab === targetTab) {
|
||
content.classList.add('active');
|
||
}
|
||
});
|
||
// 切换到技能标签时加载技能
|
||
if (targetTab === 'skills') {
|
||
this.loadSkills();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ===== Model Selector Methods =====
|
||
initializeModelSelector() {
|
||
const modelSelect = document.getElementById('model-select');
|
||
if (!modelSelect) return;
|
||
|
||
// 加载模型列表
|
||
this.loadModelList();
|
||
|
||
// 监听模型选择变化
|
||
modelSelect.addEventListener('change', () => {
|
||
this.onModelChange();
|
||
});
|
||
}
|
||
|
||
loadModelList() {
|
||
const modelSelect = document.getElementById('model-select');
|
||
if (!modelSelect) return;
|
||
|
||
const stored = localStorage.getItem('model-list');
|
||
const models = stored ? JSON.parse(stored) : [];
|
||
|
||
// 保存第一个选项
|
||
const firstOption = modelSelect.firstElementChild;
|
||
modelSelect.innerHTML = '';
|
||
modelSelect.appendChild(firstOption);
|
||
|
||
models.forEach(model => {
|
||
const option = document.createElement('option');
|
||
option.value = model.id;
|
||
option.textContent = model.name;
|
||
if (model.isDefault) {
|
||
option.textContent += ' (默认)';
|
||
}
|
||
modelSelect.appendChild(option);
|
||
});
|
||
}
|
||
|
||
onModelChange() {
|
||
const modelSelect = document.getElementById('model-select');
|
||
const selectedId = modelSelect.value;
|
||
|
||
if (!selectedId) {
|
||
// 清空隐藏字段
|
||
document.getElementById('model').value = '';
|
||
document.getElementById('api-key').value = '';
|
||
document.getElementById('model-server').value = '';
|
||
// 隐藏详情
|
||
document.getElementById('model-detail-group').style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// 从 model-list 获取模型配置
|
||
const stored = localStorage.getItem('model-list');
|
||
const models = stored ? JSON.parse(stored) : [];
|
||
const selectedModel = models.find(m => m.id === selectedId);
|
||
|
||
if (selectedModel) {
|
||
// 填充隐藏字段
|
||
document.getElementById('model').value = selectedModel.model;
|
||
document.getElementById('api-key').value = selectedModel.apiKey || '';
|
||
document.getElementById('model-server').value = selectedModel.server || '';
|
||
|
||
// 显示详情
|
||
const detailGroup = document.getElementById('model-detail-group');
|
||
detailGroup.style.display = 'block';
|
||
document.getElementById('detail-server').textContent = selectedModel.server || '默认';
|
||
document.getElementById('detail-apikey').textContent = this.maskApiKey(selectedModel.apiKey);
|
||
document.getElementById('detail-provider').textContent = selectedModel.provider || '自定义';
|
||
}
|
||
|
||
// 保存设置
|
||
this.saveSettings();
|
||
}
|
||
|
||
maskApiKey(key) {
|
||
if (!key) return '未设置';
|
||
if (key.length <= 8) return '*'.repeat(key.length);
|
||
return key.substring(0, 4) + '*'.repeat(key.length - 8) + key.substring(key.length - 4);
|
||
}
|
||
|
||
restoreModelSelection() {
|
||
const modelSelect = document.getElementById('model-select');
|
||
const currentModel = document.getElementById('model').value;
|
||
|
||
if (!currentModel) {
|
||
// 尝试选择默认模型
|
||
const stored = localStorage.getItem('model-list');
|
||
const models = stored ? JSON.parse(stored) : [];
|
||
const defaultModel = models.find(m => m.isDefault);
|
||
|
||
if (defaultModel) {
|
||
modelSelect.value = defaultModel.id;
|
||
this.onModelChange();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 根据当前 model 值查找对应的模型
|
||
const stored = localStorage.getItem('model-list');
|
||
const models = stored ? JSON.parse(stored) : [];
|
||
const matchedModel = models.find(m => m.model === currentModel);
|
||
|
||
if (matchedModel) {
|
||
modelSelect.value = matchedModel.id;
|
||
this.onModelChange();
|
||
}
|
||
}
|
||
|
||
// ===== End Model Selector Methods =====
|
||
|
||
// ===== Bot Selector Methods =====
|
||
initializeBotSelector() {
|
||
const currentBot = botManager.getCurrentBot();
|
||
|
||
if (!currentBot) {
|
||
// 没有选中的 bot,检查是否有可用的 bot
|
||
if (botManager.bots.length === 0) {
|
||
// 没有 bot,显示选择模态框
|
||
this.showBotSelectionModal();
|
||
} else {
|
||
// 有 bot 但没有选中,自动选择第一个
|
||
this.selectBot(botManager.bots[0].id);
|
||
}
|
||
} else {
|
||
// 有选中的 bot
|
||
this.currentBot = currentBot;
|
||
this.updateBotSelector();
|
||
}
|
||
|
||
this.renderBotDropdown();
|
||
}
|
||
|
||
showBotSelectionModal() {
|
||
this.elements.botSelectionModal.classList.remove('hidden');
|
||
}
|
||
|
||
closeBotSelectionModal() {
|
||
this.elements.botSelectionModal.classList.add('hidden');
|
||
}
|
||
|
||
toggleBotDropdown() {
|
||
if (botManager.bots.length === 0) {
|
||
window.location.href = 'bot-manager.html';
|
||
return;
|
||
}
|
||
|
||
this.elements.botSelectorDropdown.classList.toggle('show');
|
||
this.elements.botSelectorTrigger.classList.toggle('active');
|
||
this.renderBotDropdown();
|
||
}
|
||
|
||
renderBotDropdown() {
|
||
const dropdown = this.elements.botSelectorDropdown;
|
||
dropdown.innerHTML = '';
|
||
|
||
botManager.bots.forEach(bot => {
|
||
const item = document.createElement('div');
|
||
item.className = 'bot-selector-item';
|
||
if (this.currentBot && this.currentBot.id === bot.id) {
|
||
item.classList.add('selected');
|
||
}
|
||
|
||
item.innerHTML = `
|
||
<div class="bot-selector-item-icon">
|
||
<i data-lucide="bot" style="width: 16px; height: 16px;"></i>
|
||
</div>
|
||
<div class="bot-selector-item-info">
|
||
<div class="bot-selector-item-name">${this.escapeHtml(bot.name)}</div>
|
||
<div class="bot-selector-item-id">${this.escapeHtml(bot.botId)}</div>
|
||
</div>
|
||
`;
|
||
|
||
item.addEventListener('click', () => this.selectBot(bot.id));
|
||
dropdown.appendChild(item);
|
||
});
|
||
|
||
// 添加"管理 Bots"选项
|
||
const manageItem = document.createElement('div');
|
||
manageItem.className = 'bot-selector-manage';
|
||
manageItem.innerHTML = `
|
||
<i data-lucide="settings" style="width: 16px; height: 16px;"></i>
|
||
管理 Bots
|
||
`;
|
||
manageItem.addEventListener('click', () => {
|
||
window.location.href = 'bot-manager.html';
|
||
});
|
||
dropdown.appendChild(manageItem);
|
||
|
||
lucide.createIcons();
|
||
}
|
||
|
||
selectBot(botInternalId) {
|
||
const bot = botManager.setCurrentBot(botInternalId);
|
||
if (bot) {
|
||
this.currentBot = bot;
|
||
this.updateBotSelector();
|
||
// 重新加载 MCP 服务器配置
|
||
this.customMcpServers = this.loadCustomMcpServers();
|
||
this.loadSettings();
|
||
// 清空当前消息和会话
|
||
this.messages = [];
|
||
this.currentSessionId = null;
|
||
// 重新加载聊天历史
|
||
this.loadChatHistory();
|
||
// 清空聊天容器并显示欢迎界面
|
||
this.elements.chatContainer.innerHTML = '';
|
||
this.elements.chatContainer.appendChild(this.elements.welcomeScreen);
|
||
this.elements.welcomeScreen.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
updateBotSelector() {
|
||
if (this.currentBot) {
|
||
this.elements.botSelectorCurrent.textContent = this.currentBot.name;
|
||
} else {
|
||
this.elements.botSelectorCurrent.textContent = '选择 Bot...';
|
||
}
|
||
}
|
||
|
||
// ===== End Bot Selector Methods =====
|
||
|
||
loadCustomMcpServers() {
|
||
const settingsKey = botManager.getBotSettingsKey();
|
||
if (!settingsKey) return [];
|
||
|
||
const mcpKey = `${settingsKey}-mcp`;
|
||
const stored = localStorage.getItem(mcpKey);
|
||
if (stored) {
|
||
try {
|
||
return JSON.parse(stored);
|
||
} catch (e) {
|
||
console.error('Failed to load MCP servers:', e);
|
||
return [];
|
||
}
|
||
}
|
||
return [];
|
||
}
|
||
|
||
saveCustomMcpServers() {
|
||
const settingsKey = botManager.getBotSettingsKey();
|
||
if (!settingsKey) return;
|
||
|
||
const mcpKey = `${settingsKey}-mcp`;
|
||
localStorage.setItem(mcpKey, JSON.stringify(this.customMcpServers));
|
||
this.updateMcpCount();
|
||
}
|
||
|
||
updateMcpCount() {
|
||
const count = this.customMcpServers.filter(s => s.enabled).length;
|
||
const countEl = document.getElementById('mcp-count');
|
||
if (countEl) countEl.textContent = count;
|
||
}
|
||
|
||
openMCPManager() {
|
||
this.renderMcpManagerList();
|
||
this.elements.mcpManagerModal.classList.add('active');
|
||
}
|
||
|
||
closeMCPManager() {
|
||
this.elements.mcpManagerModal.classList.remove('active');
|
||
this.saveSettings(); // 保存设置以更新 mcp-settings
|
||
}
|
||
|
||
renderMcpManagerList() {
|
||
const container = this.elements.mcpManagerList;
|
||
container.innerHTML = '';
|
||
|
||
if (this.customMcpServers.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="mcp-empty-state">
|
||
<div class="mcp-empty-state-icon">
|
||
<i data-lucide="server" style="width: 24px; height: 24px;"></i>
|
||
</div>
|
||
<p>暂无配置的 MCP 服务器</p>
|
||
<p style="font-size: 12px; margin-top: 8px;">点击上方"添加服务器"按钮添加</p>
|
||
</div>
|
||
`;
|
||
lucide.createIcons();
|
||
return;
|
||
}
|
||
|
||
this.customMcpServers.forEach((server, index) => {
|
||
const item = document.createElement('div');
|
||
item.className = `mcp-manager-item ${server.enabled ? 'enabled' : ''}`;
|
||
|
||
// 从 config 中获取 server_type 作为名称
|
||
const serverName = server.config?.server_type || `MCP 服务器 ${index + 1}`;
|
||
const configJson = JSON.stringify(server.config, null, 2);
|
||
|
||
item.innerHTML = `
|
||
<input type="checkbox" class="mcp-manager-checkbox" ${server.enabled ? 'checked' : ''}>
|
||
<div class="mcp-manager-info">
|
||
<div class="mcp-manager-name">${this.escapeHtml(serverName)}</div>
|
||
<div class="mcp-manager-config">${this.escapeHtml(configJson)}</div>
|
||
</div>
|
||
<div class="mcp-manager-actions">
|
||
<button type="button" class="mcp-manager-action-btn edit" data-index="${index}" title="编辑">
|
||
<i data-lucide="pencil" style="width: 14px; height: 14px;"></i>
|
||
</button>
|
||
<button type="button" class="mcp-manager-action-btn delete" data-index="${index}" title="删除">
|
||
<i data-lucide="trash-2" style="width: 14px; height: 14px;"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
container.appendChild(item);
|
||
|
||
// 事件监听
|
||
const checkbox = item.querySelector('.mcp-manager-checkbox');
|
||
checkbox.addEventListener('change', () => {
|
||
server.enabled = checkbox.checked;
|
||
item.classList.toggle('enabled', checkbox.checked);
|
||
this.saveCustomMcpServers();
|
||
});
|
||
|
||
const editBtn = item.querySelector('.edit');
|
||
editBtn.addEventListener('click', () => this.openMcpEditModal(index));
|
||
|
||
const deleteBtn = item.querySelector('.delete');
|
||
deleteBtn.addEventListener('click', () => this.deleteMcpServer(index));
|
||
});
|
||
|
||
lucide.createIcons();
|
||
}
|
||
|
||
openMcpEditModal(index = -1) {
|
||
this.editingMcpIndex = index;
|
||
|
||
if (index >= 0) {
|
||
// 编辑模式 - 加载现有配置
|
||
const server = this.customMcpServers[index];
|
||
this.elements.mcpEditTitle.textContent = '编辑 MCP 服务器';
|
||
// 尝试转换为简化格式显示
|
||
this.elements.mcpEditJson.value = this.toSimplifiedFormat(server.config);
|
||
} else {
|
||
// 添加模式 - 空模板
|
||
this.elements.mcpEditTitle.textContent = '添加 MCP 服务器';
|
||
this.elements.mcpEditJson.value = '';
|
||
}
|
||
|
||
this.elements.mcpEditModal.classList.add('active');
|
||
}
|
||
|
||
// 将内部格式转换为简化格式(用于显示)
|
||
toSimplifiedFormat(config) {
|
||
const serverType = config.server_type || 'MCP-Server';
|
||
const simplified = {
|
||
[serverType]: { ...config }
|
||
};
|
||
// 移除 server_type 字段,避免重复
|
||
delete simplified[serverType].server_type;
|
||
return JSON.stringify(simplified, null, 2);
|
||
}
|
||
|
||
closeMcpEditModal() {
|
||
this.elements.mcpEditModal.classList.remove('active');
|
||
this.editingMcpIndex = -1;
|
||
}
|
||
|
||
saveMcpEdit() {
|
||
const jsonText = this.elements.mcpEditJson.value.trim();
|
||
|
||
if (!jsonText) {
|
||
alert('请输入 MCP 配置 JSON');
|
||
return;
|
||
}
|
||
|
||
let config;
|
||
try {
|
||
config = JSON.parse(jsonText);
|
||
} catch (e) {
|
||
alert('JSON 格式错误: ' + e.message);
|
||
return;
|
||
}
|
||
|
||
// 支持简化格式: { "ServerName": { ...config } }
|
||
if (this.isSimplifiedFormat(config)) {
|
||
const serverName = Object.keys(config)[0];
|
||
const serverConfig = config[serverName];
|
||
config = {
|
||
...serverConfig,
|
||
server_type: serverName
|
||
};
|
||
}
|
||
|
||
// 生成 server_type(如果没有的话)
|
||
if (!config.server_type) {
|
||
config.server_type = `mcp-server-${Date.now()}`;
|
||
}
|
||
|
||
if (this.editingMcpIndex >= 0) {
|
||
// 编辑现有服务器
|
||
this.customMcpServers[this.editingMcpIndex].config = config;
|
||
} else {
|
||
// 添加新服务器
|
||
this.customMcpServers.push({
|
||
config: config,
|
||
enabled: true
|
||
});
|
||
}
|
||
|
||
this.saveCustomMcpServers();
|
||
this.renderMcpManagerList();
|
||
this.closeMcpEditModal();
|
||
}
|
||
|
||
// 检查是否为简化格式(单层结构,服务器名作为 key)
|
||
isSimplifiedFormat(obj) {
|
||
if (!obj || typeof obj !== 'object') return false;
|
||
|
||
const keys = Object.keys(obj);
|
||
if (keys.length !== 1) return false;
|
||
|
||
const value = obj[keys[0]];
|
||
// 简化格式特征:值为对象
|
||
return value && typeof value === 'object';
|
||
}
|
||
|
||
deleteMcpServer(index) {
|
||
if (confirm('确定要删除此 MCP 服务器吗?')) {
|
||
this.customMcpServers.splice(index, 1);
|
||
this.saveCustomMcpServers();
|
||
this.renderMcpManagerList();
|
||
}
|
||
}
|
||
|
||
toggleSidebar() {
|
||
this.elements.sidebar.classList.toggle('open');
|
||
this.elements.sidebarOverlay.classList.toggle('active');
|
||
|
||
// Show/hide close button on mobile
|
||
if (window.innerWidth <= 1024) {
|
||
this.elements.sidebarCloseBtn.style.display =
|
||
this.elements.sidebar.classList.contains('open') ? 'flex' : 'none';
|
||
}
|
||
}
|
||
|
||
closeSidebar() {
|
||
this.elements.sidebar.classList.remove('open');
|
||
this.elements.sidebarOverlay.classList.remove('active');
|
||
this.elements.sidebarCloseBtn.style.display = 'none';
|
||
}
|
||
|
||
toggleTheme() {
|
||
document.documentElement.classList.toggle('dark');
|
||
const isDark = document.documentElement.classList.contains('dark');
|
||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||
this.updateThemeIcon(isDark);
|
||
}
|
||
|
||
loadTheme() {
|
||
const theme = localStorage.getItem('theme') || 'light';
|
||
if (theme === 'dark') {
|
||
document.documentElement.classList.add('dark');
|
||
}
|
||
this.updateThemeIcon(theme === 'dark');
|
||
}
|
||
|
||
updateThemeIcon(isDark) {
|
||
if (!this.elements.themeToggle) return;
|
||
const icon = this.elements.themeToggle.querySelector('i');
|
||
if (!icon) return;
|
||
icon.setAttribute('data-lucide', isDark ? 'sun' : 'moon');
|
||
lucide.createIcons();
|
||
}
|
||
|
||
openSettings() {
|
||
this.elements.settingsModal.classList.add('active');
|
||
// 打开设置时加载技能列表
|
||
this.loadSkills();
|
||
// 重新加载模型列表并恢复选择
|
||
this.loadModelList();
|
||
this.restoreModelSelection();
|
||
}
|
||
|
||
closeSettings() {
|
||
this.elements.settingsModal.classList.remove('active');
|
||
}
|
||
|
||
saveAndCloseSettings() {
|
||
this.saveSettings();
|
||
this.closeSettings();
|
||
}
|
||
|
||
loadSettings() {
|
||
const settingsKey = botManager.getBotSettingsKey();
|
||
if (!settingsKey) return;
|
||
|
||
const settings = localStorage.getItem(settingsKey);
|
||
if (settings) {
|
||
try {
|
||
const parsed = JSON.parse(settings);
|
||
Object.keys(parsed).forEach(key => {
|
||
// Skip session-id, model, api-key, model-server as they are managed separately
|
||
if (['session-id', 'model', 'api-key', 'model-server'].includes(key)) return;
|
||
|
||
const element = document.getElementById(key);
|
||
if (element) {
|
||
if (element.type === 'checkbox') {
|
||
element.checked = parsed[key];
|
||
} else {
|
||
element.value = parsed[key];
|
||
}
|
||
}
|
||
});
|
||
|
||
// Handle custom API domain visibility
|
||
if (this.elements.apiDomain.value === 'custom') {
|
||
this.elements.customApiDomainGroup.style.display = 'block';
|
||
}
|
||
|
||
// 恢复模型相关的字段(从模型列表中读取)
|
||
if (parsed['model']) {
|
||
document.getElementById('model').value = parsed['model'];
|
||
}
|
||
if (parsed['api-key']) {
|
||
document.getElementById('api-key').value = parsed['api-key'];
|
||
}
|
||
if (parsed['model-server']) {
|
||
document.getElementById('model-server').value = parsed['model-server'];
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load settings:', e);
|
||
}
|
||
}
|
||
|
||
// 更新 MCP 计数显示
|
||
this.updateMcpCount();
|
||
}
|
||
|
||
saveSettings() {
|
||
const settingsKey = botManager.getBotSettingsKey();
|
||
if (!settingsKey) return;
|
||
|
||
// 获取选中的技能
|
||
const selectedSkills = [];
|
||
document.querySelectorAll('.skill-item input:checked').forEach(checkbox => {
|
||
selectedSkills.push(checkbox.value);
|
||
});
|
||
|
||
// 获取启用的 MCP 服务器配置
|
||
const mcpServers = {};
|
||
this.customMcpServers.filter(s => s.enabled).forEach(server => {
|
||
const serverType = server.config?.server_type || 'unknown';
|
||
const config = server.config || {};
|
||
// 移除 server_type 避免重复,因为 server_type 已经作为 key
|
||
const { server_type, ...configWithoutType } = config;
|
||
mcpServers[serverType] = configWithoutType;
|
||
});
|
||
|
||
const mcpSettingsValue = Object.keys(mcpServers).length > 0
|
||
? JSON.stringify([{ mcpServers: mcpServers }])
|
||
: '';
|
||
|
||
const settings = {
|
||
'model': document.getElementById('model').value,
|
||
'api-key': document.getElementById('api-key').value,
|
||
'model-server': document.getElementById('model-server').value,
|
||
'api-domain': document.getElementById('api-domain').value,
|
||
'custom-api-domain': document.getElementById('custom-api-domain').value,
|
||
'language': document.getElementById('language').value,
|
||
'robot-type': document.getElementById('robot-type').value,
|
||
'dataset-ids': document.getElementById('dataset-ids').value,
|
||
'system-prompt': document.getElementById('system-prompt').value,
|
||
'user-identifier': document.getElementById('user-identifier').value,
|
||
'enable-memori': document.getElementById('enable-memori').checked,
|
||
'skills': selectedSkills.join(','),
|
||
'mcp-settings': mcpSettingsValue,
|
||
'tool-response': document.getElementById('tool-response').checked
|
||
};
|
||
localStorage.setItem(settingsKey, JSON.stringify(settings));
|
||
}
|
||
|
||
async loadSkills() {
|
||
const container = this.elements.skillsContainer;
|
||
// 获取当前选中的 bot,使用其 botId(用户创建时填写的 bot_id)
|
||
const currentBot = botManager.getCurrentBot();
|
||
const botId = currentBot?.botId || '';
|
||
|
||
// 如果没有 botId,显示提示而不是使用 'test'
|
||
if (!botId) {
|
||
container.innerHTML = '<div class="skills-loading">请先选择一个 Bot</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = '<div class="skills-loading">加载中...</div>';
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/skill/list?bot_id=${botId}`);
|
||
if (!response.ok) {
|
||
throw new Error('Failed to load skills');
|
||
}
|
||
|
||
const data = await response.json();
|
||
container.innerHTML = '';
|
||
|
||
if (data.skills && data.skills.length > 0) {
|
||
data.skills.forEach(skill => {
|
||
const item = document.createElement('label');
|
||
item.className = 'skill-item';
|
||
|
||
// 获取当前已选择的技能
|
||
const currentSkills = document.getElementById('skills').value.split(',').filter(s => s);
|
||
const isChecked = currentSkills.includes(skill.name);
|
||
|
||
item.innerHTML = `
|
||
<input type="checkbox" value="${skill.name}" ${isChecked ? 'checked' : ''}>
|
||
<div class="skill-item-label">
|
||
<div>${this.escapeHtml(skill.name)}</div>
|
||
<div class="skill-item-desc">${this.escapeHtml(skill.description || '')}</div>
|
||
</div>
|
||
`;
|
||
|
||
// 事件监听
|
||
const checkbox = item.querySelector('input');
|
||
checkbox.addEventListener('change', () => {
|
||
item.classList.toggle('selected', checkbox.checked);
|
||
this.updateSelectedSkills();
|
||
});
|
||
|
||
if (isChecked) {
|
||
item.classList.add('selected');
|
||
}
|
||
|
||
container.appendChild(item);
|
||
});
|
||
} else {
|
||
container.innerHTML = '<div class="skills-loading">暂无可用技能</div>';
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load skills:', error);
|
||
container.innerHTML = '<div class="skills-loading">加载失败,请重试</div>';
|
||
}
|
||
}
|
||
|
||
updateSelectedSkills() {
|
||
const selectedSkills = [];
|
||
document.querySelectorAll('.skill-item input:checked').forEach(checkbox => {
|
||
selectedSkills.push(checkbox.value);
|
||
});
|
||
document.getElementById('skills').value = selectedSkills.join(',');
|
||
this.saveSettings();
|
||
}
|
||
|
||
loadChatHistory() {
|
||
const settingsKey = botManager.getBotSettingsKey();
|
||
if (!settingsKey) return;
|
||
|
||
const historyKey = `${settingsKey}-sessions`;
|
||
const historyData = localStorage.getItem(historyKey);
|
||
if (historyData) {
|
||
try {
|
||
this.allSessions = JSON.parse(historyData);
|
||
} catch (e) {
|
||
this.allSessions = [];
|
||
}
|
||
} else {
|
||
this.allSessions = [];
|
||
}
|
||
|
||
// Load or create current session
|
||
const lastSessionId = localStorage.getItem('current-session-id');
|
||
if (lastSessionId && this.getSessionById(lastSessionId)) {
|
||
this.loadSession(lastSessionId);
|
||
} else if (this.allSessions.length === 0) {
|
||
this.startNewChat();
|
||
} else {
|
||
this.loadSession(this.allSessions[0].id);
|
||
}
|
||
|
||
this.renderChatHistory();
|
||
}
|
||
|
||
getSessionById(sessionId) {
|
||
return this.allSessions.find(s => s.id === sessionId);
|
||
}
|
||
|
||
saveCurrentSession() {
|
||
if (!this.currentSessionId) return;
|
||
|
||
const settingsKey = botManager.getBotSettingsKey();
|
||
if (!settingsKey) return;
|
||
|
||
const historyKey = `${settingsKey}-sessions`;
|
||
|
||
const sessionData = this.getSessionById(this.currentSessionId);
|
||
if (sessionData) {
|
||
sessionData.messages = this.messages;
|
||
sessionData.updatedAt = Date.now();
|
||
// Update title from first user message if exists
|
||
if (this.messages.length > 0) {
|
||
const firstUserMessage = this.messages.find(m => m.role === 'user');
|
||
if (firstUserMessage) {
|
||
sessionData.title = firstUserMessage.content.slice(0, 30) + (firstUserMessage.content.length > 30 ? '...' : '');
|
||
}
|
||
}
|
||
} else {
|
||
// Create new session
|
||
const newSession = {
|
||
id: this.currentSessionId,
|
||
title: this.messages.length > 0 ? this.messages[0].content.slice(0, 30) + '...' : '新对话',
|
||
messages: this.messages,
|
||
createdAt: Date.now(),
|
||
updatedAt: Date.now()
|
||
};
|
||
this.allSessions.unshift(newSession);
|
||
}
|
||
|
||
localStorage.setItem(historyKey, JSON.stringify(this.allSessions));
|
||
}
|
||
|
||
loadSession(sessionId) {
|
||
const session = this.getSessionById(sessionId);
|
||
if (!session) return;
|
||
|
||
// Save current session before switching
|
||
if (this.currentSessionId && this.currentSessionId !== sessionId) {
|
||
this.saveCurrentSession();
|
||
}
|
||
|
||
this.currentSessionId = sessionId;
|
||
this.messages = session.messages || [];
|
||
localStorage.setItem('current-session-id', sessionId);
|
||
|
||
// Clear and rebuild chat container
|
||
this.elements.chatContainer.innerHTML = '';
|
||
this.elements.chatContainer.appendChild(this.elements.welcomeScreen);
|
||
|
||
if (this.messages.length === 0) {
|
||
this.elements.welcomeScreen.style.display = 'flex';
|
||
} else {
|
||
this.elements.welcomeScreen.style.display = 'none';
|
||
this.messages.forEach(msg => {
|
||
const messageEl = this.createMessageElement(msg.role, msg.content);
|
||
this.elements.chatContainer.appendChild(messageEl);
|
||
});
|
||
this.scrollToBottom();
|
||
}
|
||
|
||
this.renderChatHistory();
|
||
}
|
||
|
||
deleteSession(sessionId, event) {
|
||
if (event) event.stopPropagation();
|
||
|
||
const settingsKey = botManager.getBotSettingsKey();
|
||
if (!settingsKey) return;
|
||
|
||
const historyKey = `${settingsKey}-sessions`;
|
||
|
||
this.allSessions = this.allSessions.filter(s => s.id !== sessionId);
|
||
localStorage.setItem(historyKey, JSON.stringify(this.allSessions));
|
||
|
||
if (this.currentSessionId === sessionId) {
|
||
if (this.allSessions.length > 0) {
|
||
this.loadSession(this.allSessions[0].id);
|
||
} else {
|
||
this.startNewChat();
|
||
}
|
||
}
|
||
|
||
this.renderChatHistory();
|
||
}
|
||
|
||
renderChatHistory() {
|
||
const chatHistoryEl = document.getElementById('chat-history');
|
||
if (!chatHistoryEl) return;
|
||
|
||
// Group sessions by date
|
||
const now = Date.now();
|
||
const today = [];
|
||
const last7Days = [];
|
||
const older = [];
|
||
|
||
this.allSessions.forEach(session => {
|
||
const daysSince = (now - session.updatedAt) / (1000 * 60 * 60 * 24);
|
||
if (daysSince < 1) {
|
||
today.push(session);
|
||
} else if (daysSince < 7) {
|
||
last7Days.push(session);
|
||
} else {
|
||
older.push(session);
|
||
}
|
||
});
|
||
|
||
let html = '';
|
||
|
||
const renderSection = (title, sessions) => {
|
||
if (sessions.length === 0) return '';
|
||
let sectionHtml = `<div class="chat-history-section">
|
||
<div class="chat-history-title">${title}</div>`;
|
||
sessions.forEach(session => {
|
||
const isActive = session.id === this.currentSessionId ? 'active' : '';
|
||
const icon = this.getSessionIcon(session);
|
||
sectionHtml += `
|
||
<div class="chat-history-item ${isActive}" data-session-id="${session.id}">
|
||
<div class="chat-history-item-icon">${icon}</div>
|
||
<div class="chat-history-item-content">
|
||
<div class="chat-history-item-title">${this.escapeHtml(session.title)}</div>
|
||
<div class="chat-history-item-preview">${this.getPreviewText(session.messages)}</div>
|
||
</div>
|
||
<div class="chat-history-item-delete" data-delete-session="${session.id}">
|
||
<i data-lucide="trash-2" style="width: 14px; height: 14px;"></i>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
sectionHtml += '</div>';
|
||
return sectionHtml;
|
||
};
|
||
|
||
html += renderSection('今天', today);
|
||
html += renderSection('过去 7 天', last7Days);
|
||
html += renderSection('更早', older);
|
||
|
||
chatHistoryEl.innerHTML = html;
|
||
|
||
// Add click handlers
|
||
chatHistoryEl.querySelectorAll('.chat-history-item').forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.chat-history-item-delete')) {
|
||
this.loadSession(item.dataset.sessionId);
|
||
}
|
||
});
|
||
});
|
||
|
||
chatHistoryEl.querySelectorAll('.chat-history-item-delete').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
this.deleteSession(btn.dataset.deleteSession, e);
|
||
});
|
||
});
|
||
|
||
lucide.createIcons();
|
||
}
|
||
|
||
getSessionIcon(session) {
|
||
const title = session.title.toLowerCase();
|
||
if (title.includes('代码') || title.includes('python') || title.includes('js') || title.includes('api')) {
|
||
return '<i data-lucide="code" style="width: 16px; height: 16px;"></i>';
|
||
} else if (title.includes('学习') || title.includes('教程')) {
|
||
return '<i data-lucide="book-open" style="width: 16px; height: 16px;"></i>';
|
||
} else {
|
||
return '<i data-lucide="message-square" style="width: 16px; height: 16px;"></i>';
|
||
}
|
||
}
|
||
|
||
getPreviewText(messages) {
|
||
if (!messages || messages.length === 0) return '暂无消息';
|
||
const lastMessage = messages[messages.length - 1];
|
||
let text = lastMessage.content;
|
||
// Remove markdown and tool tags for preview
|
||
text = text.replace(/\[TOOL_CALL\][\s\S]*?\[TOOL_RESPONSE\]/g, '[使用工具]');
|
||
text = text.replace(/```[\s\S]*?```/g, '[代码]');
|
||
text = text.replace(/\*\*/g, '').replace(/\*/g, '');
|
||
if (text.length > 40) text = text.slice(0, 40) + '...';
|
||
return text;
|
||
}
|
||
|
||
startNewChat() {
|
||
// Save current session before starting new one
|
||
if (this.currentSessionId) {
|
||
this.saveCurrentSession();
|
||
}
|
||
|
||
// Create new session
|
||
this.currentSessionId = this.generateUUID();
|
||
this.messages = [];
|
||
|
||
localStorage.setItem('current-session-id', this.currentSessionId);
|
||
|
||
// Clear chat container
|
||
this.elements.chatContainer.innerHTML = '';
|
||
this.elements.chatContainer.appendChild(this.elements.welcomeScreen);
|
||
this.elements.welcomeScreen.style.display = 'flex';
|
||
|
||
this.renderChatHistory();
|
||
}
|
||
|
||
getSettings() {
|
||
// 辅助函数:安全获取元素值
|
||
const getValue = (id, defaultValue = '') => {
|
||
const el = document.getElementById(id);
|
||
return el?.value ?? defaultValue;
|
||
};
|
||
|
||
// 辅助函数:安全获取 checkbox 状态
|
||
const getChecked = (id, defaultValue = false) => {
|
||
const el = document.getElementById(id);
|
||
return el?.checked ?? defaultValue;
|
||
};
|
||
|
||
const apiDomainSelect = document.getElementById('api-domain');
|
||
let apiUrl;
|
||
|
||
if (!apiDomainSelect) {
|
||
apiUrl = `${window.location.origin}/api/v1/chat/completions`;
|
||
} else if (apiDomainSelect.value === 'current') {
|
||
apiUrl = `${window.location.origin}/api/v1/chat/completions`;
|
||
} else if (apiDomainSelect.value === 'custom') {
|
||
const customDomain = getValue('custom-api-domain');
|
||
if (customDomain) {
|
||
let domain = customDomain.trim();
|
||
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
|
||
domain = 'https://' + domain;
|
||
}
|
||
apiUrl = `${domain}/api/v1/chat/completions`;
|
||
} else {
|
||
apiUrl = `${window.location.origin}/api/v1/chat/completions`;
|
||
}
|
||
} else {
|
||
apiUrl = `${apiDomainSelect.value}/api/v1/chat/completions`;
|
||
}
|
||
|
||
const datasetIdsValue = getValue('dataset-ids');
|
||
const datasetIds = datasetIdsValue
|
||
.split(',')
|
||
.map(id => id.trim())
|
||
.filter(id => id);
|
||
|
||
const skillsValue = getValue('skills');
|
||
const skills = skillsValue
|
||
.split(',')
|
||
.map(skill => skill.trim())
|
||
.filter(skill => skill);
|
||
|
||
// 直接从 customMcpServers 构建 mcpSettings
|
||
let mcpSettings = [];
|
||
const mcpServers = {};
|
||
this.customMcpServers.filter(s => s.enabled).forEach(server => {
|
||
const serverType = server.config?.server_type || 'unknown';
|
||
const config = server.config || {};
|
||
// 移除 server_type 避免重复,因为 server_type 已经作为 key
|
||
const { server_type, ...configWithoutType } = config;
|
||
mcpServers[serverType] = configWithoutType;
|
||
});
|
||
|
||
if (Object.keys(mcpServers).length > 0) {
|
||
mcpSettings = [{ mcpServers: mcpServers }];
|
||
}
|
||
|
||
// Use current session ID
|
||
let sessionId = this.currentSessionId;
|
||
if (!sessionId) {
|
||
sessionId = this.generateUUID();
|
||
this.currentSessionId = sessionId;
|
||
localStorage.setItem('current-session-id', sessionId);
|
||
}
|
||
|
||
return {
|
||
apiUrl,
|
||
model: getValue('model'),
|
||
apiKey: getValue('api-key'),
|
||
modelServer: getValue('model-server'),
|
||
botId: this.currentBot?.botId || '',
|
||
language: getValue('language', 'zh-CN'),
|
||
robotType: getValue('robot-type'),
|
||
datasetIds,
|
||
systemPrompt: getValue('system-prompt'),
|
||
sessionId,
|
||
userIdentifier: getValue('user-identifier'),
|
||
enableMemori: getChecked('enable-memori'),
|
||
skills,
|
||
mcpSettings,
|
||
toolResponse: getChecked('tool-response')
|
||
};
|
||
}
|
||
|
||
generateUUID() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
const r = Math.random() * 16 | 0;
|
||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
exportSettings() {
|
||
const exportData = {
|
||
version: '1.0',
|
||
exportDate: new Date().toISOString(),
|
||
botList: JSON.parse(localStorage.getItem('bot-list') || '[]'),
|
||
modelList: JSON.parse(localStorage.getItem('model-list') || '[]'),
|
||
theme: localStorage.getItem('theme'),
|
||
botSettings: {},
|
||
botMcpServers: {},
|
||
botSessions: {}
|
||
};
|
||
|
||
// Collect per-bot data
|
||
const bots = exportData.botList;
|
||
bots.forEach(bot => {
|
||
const settingsKey = `bot-settings-${bot.botId}`;
|
||
exportData.botSettings[bot.botId] = localStorage.getItem(settingsKey);
|
||
exportData.botMcpServers[bot.botId] = localStorage.getItem(`${settingsKey}-mcp`);
|
||
exportData.botSessions[bot.botId] = localStorage.getItem(`${settingsKey}-sessions`);
|
||
});
|
||
|
||
// Download as JSON file
|
||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `ai-chat-backup-${new Date().toISOString().split('T')[0]}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
importSettings(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
try {
|
||
const data = JSON.parse(e.target.result);
|
||
|
||
// Validate structure
|
||
if (!data.version || !Array.isArray(data.botList) || !Array.isArray(data.modelList)) {
|
||
alert('无效的配置文件格式');
|
||
return;
|
||
}
|
||
|
||
// Confirm import
|
||
const botCount = data.botList.length;
|
||
const modelCount = data.modelList.length;
|
||
if (!confirm(`确定要导入配置吗?这将覆盖现有数据。\n\n包含 ${botCount} 个机器人,${modelCount} 个模型配置`)) {
|
||
return;
|
||
}
|
||
|
||
// Restore global configurations
|
||
localStorage.setItem('bot-list', JSON.stringify(data.botList));
|
||
localStorage.setItem('model-list', JSON.stringify(data.modelList));
|
||
if (data.theme) {
|
||
localStorage.setItem('theme', data.theme);
|
||
}
|
||
|
||
// Restore per-bot data
|
||
if (data.botSettings) {
|
||
Object.keys(data.botSettings).forEach(botId => {
|
||
if (data.botSettings[botId]) {
|
||
localStorage.setItem(`bot-settings-${botId}`, data.botSettings[botId]);
|
||
}
|
||
if (data.botMcpServers && data.botMcpServers[botId]) {
|
||
localStorage.setItem(`bot-settings-${botId}-mcp`, data.botMcpServers[botId]);
|
||
}
|
||
if (data.botSessions && data.botSessions[botId]) {
|
||
localStorage.setItem(`bot-settings-${botId}-sessions`, data.botSessions[botId]);
|
||
}
|
||
});
|
||
}
|
||
|
||
alert('配置导入成功!页面将重新加载。');
|
||
location.reload();
|
||
} catch (err) {
|
||
alert('导入失败:' + err.message);
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
|
||
// Reset file input
|
||
event.target.value = '';
|
||
}
|
||
|
||
updateStatus(status, text) {
|
||
this.elements.statusDot.className = 'status-dot';
|
||
if (status) this.elements.statusDot.classList.add(status);
|
||
this.elements.statusText.textContent = text;
|
||
}
|
||
|
||
async sendMessage() {
|
||
if (this.isProcessing) return;
|
||
|
||
const message = this.elements.chatInput.value.trim();
|
||
if (!message) return;
|
||
|
||
// Hide welcome screen
|
||
this.elements.welcomeScreen.style.display = 'none';
|
||
|
||
// Add user message
|
||
this.addMessage('user', message);
|
||
this.elements.chatInput.value = '';
|
||
this.elements.chatInput.style.height = 'auto';
|
||
this.setProcessingState(true);
|
||
|
||
try {
|
||
await this.callAPI(message);
|
||
// Save session after successful API call
|
||
this.saveCurrentSession();
|
||
} catch (error) {
|
||
this.addError(`连接错误: ${error.message}`);
|
||
this.updateStatus('error', '连接失败');
|
||
} finally {
|
||
this.setProcessingState(false);
|
||
}
|
||
}
|
||
|
||
addMessage(role, content) {
|
||
// Store message in memory
|
||
this.messages.push({ role, content });
|
||
|
||
const messageEl = this.createMessageElement(role, content);
|
||
this.elements.chatContainer.appendChild(messageEl);
|
||
this.scrollToBottom();
|
||
return messageEl;
|
||
}
|
||
|
||
createMessageElement(role, content) {
|
||
const container = document.createElement('div');
|
||
container.className = `message ${role}`;
|
||
|
||
const avatar = document.createElement('div');
|
||
avatar.className = 'message-avatar';
|
||
if (role === 'assistant') {
|
||
avatar.innerHTML = '<i data-lucide="bot" style="width: 20px; height: 20px;"></i>';
|
||
} else {
|
||
avatar.innerHTML = '<i data-lucide="user" style="width: 20px; height: 20px;"></i>';
|
||
}
|
||
|
||
const contentEl = document.createElement('div');
|
||
contentEl.className = 'message-content';
|
||
this.updateMessageContent(contentEl, content, role === 'user');
|
||
|
||
container.appendChild(avatar);
|
||
container.appendChild(contentEl);
|
||
|
||
lucide.createIcons();
|
||
return container;
|
||
}
|
||
|
||
addTypingIndicator() {
|
||
const container = document.createElement('div');
|
||
container.className = 'message assistant';
|
||
container.id = 'typing-indicator';
|
||
|
||
const avatar = document.createElement('div');
|
||
avatar.className = 'message-avatar';
|
||
avatar.innerHTML = '<i data-lucide="bot" style="width: 20px; height: 20px;"></i>';
|
||
|
||
const contentEl = document.createElement('div');
|
||
contentEl.className = 'typing-indicator';
|
||
contentEl.innerHTML = `
|
||
<span>AI正在思考</span>
|
||
<div class="typing-dots">
|
||
<div class="typing-dot"></div>
|
||
<div class="typing-dot"></div>
|
||
<div class="typing-dot"></div>
|
||
</div>
|
||
`;
|
||
|
||
container.appendChild(avatar);
|
||
container.appendChild(contentEl);
|
||
|
||
this.elements.chatContainer.appendChild(container);
|
||
this.scrollToBottom();
|
||
|
||
lucide.createIcons();
|
||
return container;
|
||
}
|
||
|
||
removeTypingIndicator() {
|
||
const indicator = document.getElementById('typing-indicator');
|
||
if (indicator) indicator.remove();
|
||
}
|
||
|
||
addError(message) {
|
||
const errorEl = document.createElement('div');
|
||
errorEl.className = 'error-banner';
|
||
errorEl.innerHTML = `
|
||
<i data-lucide="alert-circle" style="width: 16px; height: 16px;"></i>
|
||
<span>${message}</span>
|
||
`;
|
||
this.elements.chatContainer.appendChild(errorEl);
|
||
this.scrollToBottom();
|
||
lucide.createIcons();
|
||
}
|
||
|
||
setProcessingState(isProcessing) {
|
||
this.isProcessing = isProcessing;
|
||
this.elements.sendBtn.disabled = isProcessing;
|
||
this.elements.chatInput.disabled = isProcessing;
|
||
|
||
if (isProcessing) {
|
||
this.updateStatus('connecting', '连接中');
|
||
} else {
|
||
this.updateStatus('', '已连接');
|
||
}
|
||
}
|
||
|
||
async callAPI(message) {
|
||
const settings = this.getSettings();
|
||
|
||
// Create typing indicator
|
||
this.addTypingIndicator();
|
||
|
||
// Add placeholder for assistant message first, then build messages array excluding it
|
||
let messageEl = this.addMessage('assistant', '');
|
||
let contentElement = messageEl.querySelector('.message-content');
|
||
|
||
// Build messages array for API (exclude the empty assistant message we just added)
|
||
const apiMessages = this.messages.slice(0, -1).map(msg => ({
|
||
role: msg.role,
|
||
content: msg.content
|
||
}));
|
||
|
||
const requestBody = {
|
||
messages: apiMessages,
|
||
stream: true,
|
||
model: settings.model,
|
||
model_server: settings.modelServer,
|
||
bot_id: settings.botId,
|
||
language: settings.language,
|
||
tool_response: settings.toolResponse
|
||
};
|
||
|
||
// Add optional parameters
|
||
if (settings.robotType) requestBody.robot_type = settings.robotType;
|
||
if (settings.systemPrompt) requestBody.system_prompt = settings.systemPrompt;
|
||
if (settings.sessionId) requestBody.session_id = settings.sessionId;
|
||
if (settings.userIdentifier) requestBody.user_identifier = settings.userIdentifier;
|
||
if (settings.enableMemori) requestBody.enable_memori = settings.enableMemori;
|
||
if (settings.skills?.length) requestBody.skills = settings.skills;
|
||
if (settings.datasetIds?.length) requestBody.dataset_ids = settings.datasetIds;
|
||
if (settings.mcpSettings?.length) requestBody.mcp_settings = settings.mcpSettings;
|
||
|
||
try {
|
||
const response = await fetch(settings.apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${settings.apiKey}`
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}`);
|
||
}
|
||
|
||
this.updateStatus('', '已连接');
|
||
|
||
// Remove typing indicator
|
||
this.removeTypingIndicator();
|
||
|
||
let accumulatedContent = '';
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const data = line.slice(6);
|
||
if (data === '[DONE]') continue;
|
||
|
||
try {
|
||
const parsed = JSON.parse(data);
|
||
if (parsed.choices?.[0]?.delta?.content) {
|
||
accumulatedContent += parsed.choices[0].delta.content;
|
||
this.updateMessageContent(contentElement, accumulatedContent, false);
|
||
this.scrollToBottom();
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!accumulatedContent) {
|
||
accumulatedContent = '抱歉,我无法处理您的请求。';
|
||
this.updateMessageContent(contentElement, accumulatedContent, false);
|
||
}
|
||
|
||
// Update the stored message content
|
||
this.messages[this.messages.length - 1] = { role: 'assistant', content: accumulatedContent };
|
||
|
||
} catch (error) {
|
||
this.removeTypingIndicator();
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
updateMessageContent(contentElement, content, isUserMessage) {
|
||
if (isUserMessage) {
|
||
contentElement.innerHTML = this.renderSimpleMarkdown(content);
|
||
} else {
|
||
contentElement.innerHTML = this.renderMarkdown(content);
|
||
}
|
||
|
||
// Highlight code blocks
|
||
contentElement.querySelectorAll('pre code').forEach((block) => {
|
||
hljs.highlightElement(block);
|
||
});
|
||
}
|
||
|
||
renderMarkdown(content) {
|
||
const processedContent = this.processTaggedContent(content);
|
||
|
||
marked.setOptions({
|
||
highlight: function(code, lang) {
|
||
if (lang && hljs.getLanguage(lang)) {
|
||
try {
|
||
return hljs.highlight(code, { language: lang }).value;
|
||
} catch (err) {}
|
||
}
|
||
return hljs.highlightAuto(code).value;
|
||
},
|
||
breaks: true,
|
||
gfm: true
|
||
});
|
||
|
||
return marked.parse(processedContent);
|
||
}
|
||
|
||
processTaggedContent(content) {
|
||
const parts = content.split(/\[(ANSWER|TOOL_CALL|TOOL_RESPONSE)\]/);
|
||
let processedContent = '';
|
||
let toolCallIndex = 0;
|
||
|
||
for (let i = 0; i < parts.length; i += 2) {
|
||
const tag = parts[i - 1];
|
||
const text = parts[i];
|
||
|
||
if (!text || !text.trim()) continue;
|
||
|
||
if (tag === 'ANSWER') {
|
||
processedContent += `\n\n${text.trim()}\n\n`;
|
||
} else if (tag === 'TOOL_CALL') {
|
||
const lines = text.trim().split('\n');
|
||
const toolName = lines[0] || '未知工具';
|
||
const toolData = lines.slice(1).join('\n').trim();
|
||
const toolId = `tool-call-${toolCallIndex++}`;
|
||
processedContent += `
|
||
<div class="tool-section tool-call">
|
||
<div class="tool-header collapsed" onclick="window.toggleToolSection('${toolId}')">
|
||
<span class="tool-icon"><i data-lucide="chevron-down" style="width: 14px; height: 14px;"></i></span>
|
||
<span>工具调用: <strong>${toolName}</strong></span>
|
||
</div>
|
||
<div class="tool-content collapsed" id="${toolId}">
|
||
<pre>${this.escapeHtml(toolData)}</pre>
|
||
</div>
|
||
</div>`;
|
||
} else if (tag === 'TOOL_RESPONSE') {
|
||
const lines = text.trim().split('\n');
|
||
const toolName = lines[0] || '未知工具响应';
|
||
const toolData = lines.slice(1).join('\n').trim();
|
||
const toolId = `tool-response-${toolCallIndex++}`;
|
||
processedContent += `
|
||
<div class="tool-section tool-response">
|
||
<div class="tool-header collapsed" onclick="window.toggleToolSection('${toolId}')">
|
||
<span class="tool-icon"><i data-lucide="chevron-down" style="width: 14px; height: 14px;"></i></span>
|
||
<span>工具响应: <strong>${toolName}</strong></span>
|
||
</div>
|
||
<div class="tool-content collapsed" id="${toolId}">
|
||
<pre>${this.escapeHtml(toolData)}</pre>
|
||
</div>
|
||
</div>`;
|
||
} else if (!tag) {
|
||
processedContent += text;
|
||
}
|
||
}
|
||
|
||
return processedContent;
|
||
}
|
||
|
||
escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
renderSimpleMarkdown(content) {
|
||
return content
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||
.replace(/\n/g, '<br>');
|
||
}
|
||
|
||
scrollToBottom() {
|
||
this.elements.chatContainer.scrollTop = this.elements.chatContainer.scrollHeight;
|
||
}
|
||
}
|
||
|
||
// Global function for tool section toggle
|
||
window.toggleToolSection = function(toolId) {
|
||
const content = document.getElementById(toolId);
|
||
const header = content?.previousElementSibling;
|
||
if (content && header) {
|
||
header.classList.toggle('collapsed');
|
||
content.classList.toggle('collapsed');
|
||
}
|
||
};
|
||
|
||
// Initialize app
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
new ChatApp();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|