qwen_agent/public/index.html
2026-01-16 23:05:30 +08:00

3680 lines
132 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<!-- 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://cdn.staticfile.net/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdn.staticfile.net/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Marked.js -->
<script src="https://cdn.staticfile.net/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>
</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 服务器配置,例如:&#10;{&#10; "服务器名称": {&#10; "url": "http://example.com/sse",&#10; "command": "npx",&#10; "args": ["-y", "@modelcontextprotocol/server-everything"]&#10; }&#10;}&#10;&#10;根据服务器类型填入所需字段即可' 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,
'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'),
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.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>