| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AI学习助手</title> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"> |
| |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css"> |
| |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| :root { |
| |
| --primary-color: #0f2d49; |
| --primary-light: #234a70; |
| --secondary-color: #4a6cfd; |
| --secondary-light: #7b91ff; |
| --tertiary-color: #f7f9fe; |
| |
| |
| --success-color: #10b981; |
| --success-light: #d1fae5; |
| --warning-color: #f59e0b; |
| --warning-light: #fef3c7; |
| --danger-color: #ef4444; |
| --danger-light: #fee2e2; |
| --info-color: #3b82f6; |
| --info-light: #dbeafe; |
| |
| |
| --neutral-50: #f9fafb; |
| --neutral-100: #f3f4f6; |
| --neutral-200: #e5e7eb; |
| --neutral-300: #d1d5db; |
| --neutral-400: #9ca3af; |
| --neutral-500: #6b7280; |
| --neutral-600: #4b5563; |
| --neutral-700: #374151; |
| --neutral-800: #1f2937; |
| --neutral-900: #111827; |
| |
| |
| --border-radius-sm: 0.25rem; |
| --border-radius: 0.375rem; |
| --border-radius-lg: 0.5rem; |
| --border-radius-xl: 0.75rem; |
| --border-radius-xxl: 1rem; |
| --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); |
| --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); |
| --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
| --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
| --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); |
| --transition-base: all 0.2s ease-in-out; |
| --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| --font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif; |
| } |
| |
| body { |
| font-family: var(--font-family); |
| margin: 0; |
| padding: 0; |
| height: 100vh; |
| background-color: var(--neutral-50); |
| color: var(--neutral-800); |
| display: flex; |
| flex-direction: column; |
| -webkit-font-smoothing: antialiased; |
| -moz-osx-font-smoothing: grayscale; |
| } |
| |
| |
| .header { |
| background: linear-gradient(135deg, var(--primary-color), var(--primary-light)); |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
| color: white; |
| padding: 1rem 1.5rem; |
| box-shadow: var(--shadow-md); |
| position: relative; |
| z-index: 10; |
| } |
| |
| .header-content { |
| max-width: 1600px; |
| margin: 0 auto; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .header h1 { |
| margin: 0; |
| font-size: 1.5rem; |
| font-weight: 600; |
| color: white; |
| display: flex; |
| align-items: center; |
| } |
| |
| .header h1 i { |
| margin-right: 0.75rem; |
| color: rgba(255, 255, 255, 0.8); |
| } |
| |
| .header .badge { |
| background-color: rgba(255, 255, 255, 0.15); |
| color: white; |
| font-weight: 500; |
| padding: 0.35em 0.75em; |
| font-size: 0.85rem; |
| border-radius: 9999px; |
| } |
| |
| |
| .main-container { |
| flex: 1; |
| display: flex; |
| max-width: 1600px; |
| margin: 0 auto; |
| padding: 1.5rem; |
| width: 100%; |
| box-sizing: border-box; |
| height: calc(100vh - 70px); |
| overflow: hidden; |
| } |
| |
| .chat-container { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| background-color: white; |
| border-radius: var(--border-radius-xl); |
| box-shadow: var(--shadow); |
| overflow: hidden; |
| height: 100%; |
| max-width: 800px; |
| margin: 0 auto; |
| transition: var(--transition-smooth); |
| position: relative; |
| } |
| |
| |
| .chat-messages { |
| flex: 1; |
| overflow-y: auto; |
| padding: 1.5rem; |
| display: flex; |
| flex-direction: column; |
| background-color: var(--neutral-50); |
| gap: 1rem; |
| scroll-behavior: smooth; |
| } |
| |
| |
| .chat-messages::-webkit-scrollbar { |
| width: 6px; |
| height: 6px; |
| } |
| |
| .chat-messages::-webkit-scrollbar-track { |
| background: var(--neutral-100); |
| border-radius: 10px; |
| } |
| |
| .chat-messages::-webkit-scrollbar-thumb { |
| background: var(--neutral-300); |
| border-radius: 10px; |
| } |
| |
| .chat-messages::-webkit-scrollbar-thumb:hover { |
| background: var(--neutral-400); |
| } |
| |
| |
| .message { |
| max-width: 85%; |
| border-radius: var(--border-radius-lg); |
| padding: 1rem; |
| position: relative; |
| line-height: 1.5; |
| box-shadow: var(--shadow-sm); |
| overflow-wrap: break-word; |
| word-wrap: break-word; |
| hyphens: auto; |
| } |
| |
| .message .message-content { |
| padding: 0 10px; |
| margin: 0; |
| } |
| |
| .user-message { |
| background: linear-gradient(135deg, #e9f5ff, #c2e4ff); |
| align-self: flex-end; |
| color: var(--primary-color); |
| border-bottom-right-radius: 0.2rem; |
| border-left: 1px solid rgba(74, 108, 253, 0.1); |
| border-top: 1px solid rgba(255, 255, 255, 0.5); |
| } |
| |
| .bot-message { |
| background-color: white; |
| align-self: flex-start; |
| color: var(--neutral-800); |
| border-bottom-left-radius: 0.2rem; |
| border-left: 3px solid var(--secondary-color); |
| box-shadow: var(--shadow); |
| } |
| |
| |
| .input-container { |
| padding: 1rem 1.5rem; |
| border-top: 1px solid var(--neutral-200); |
| background-color: white; |
| position: relative; |
| } |
| |
| .input-row { |
| display: flex; |
| gap: 0.75rem; |
| position: relative; |
| } |
| |
| .input-field { |
| flex: 1; |
| padding: 0.85rem 1rem; |
| border: 1px solid var(--neutral-200); |
| border-radius: var(--border-radius-lg); |
| resize: none; |
| font-size: 0.95rem; |
| box-shadow: var(--shadow-inner); |
| transition: var(--transition-base); |
| } |
| |
| .input-field:focus { |
| outline: none; |
| border-color: var(--secondary-color); |
| box-shadow: 0 0 0 3px rgba(74, 108, 253, 0.15); |
| } |
| |
| .send-button { |
| padding: 0 1.25rem; |
| background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light)); |
| color: white; |
| border: none; |
| border-radius: var(--border-radius-lg); |
| cursor: pointer; |
| font-weight: 500; |
| transition: var(--transition-base); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .send-button:hover { |
| box-shadow: 0 4px 10px rgba(74, 108, 253, 0.25); |
| transform: translateY(-1px); |
| } |
| |
| .send-button:disabled { |
| background: var(--neutral-300); |
| cursor: not-allowed; |
| transform: none; |
| box-shadow: none; |
| } |
| |
| |
| .plugin-container { |
| display: none; |
| flex-direction: column; |
| background-color: white; |
| border-radius: var(--border-radius-xl); |
| box-shadow: var(--shadow); |
| overflow: hidden; |
| height: 100%; |
| flex: 2; |
| margin-left: 1.5rem; |
| transition: var(--transition-smooth); |
| } |
| |
| .plugin-header { |
| padding: 0.85rem 1.25rem; |
| border-bottom: 1px solid var(--neutral-200); |
| background-color: var(--neutral-50); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .plugin-title { |
| margin: 0; |
| font-size: 1rem; |
| font-weight: 600; |
| color: var(--primary-color); |
| display: flex; |
| align-items: center; |
| } |
| |
| .plugin-title i { |
| margin-right: 0.5rem; |
| color: var(--secondary-color); |
| } |
| |
| .plugin-close { |
| width: 32px; |
| height: 32px; |
| background: var(--neutral-100); |
| border: none; |
| border-radius: 50%; |
| cursor: pointer; |
| font-size: 0.85rem; |
| color: var(--neutral-700); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| transition: var(--transition-base); |
| } |
| |
| .plugin-close:hover { |
| background-color: var(--neutral-200); |
| color: var(--neutral-900); |
| } |
| |
| .plugin-content { |
| flex: 1; |
| overflow-y: auto; |
| padding: 0; |
| background-color: var(--neutral-50); |
| } |
| |
| |
| .main-container.with-plugin .chat-container { |
| max-width: 380px; |
| margin: 0; |
| } |
| |
| |
| pre { |
| margin: 1rem 0; |
| padding: 1rem; |
| background-color: var(--neutral-800); |
| border-radius: var(--border-radius); |
| overflow-x: auto; |
| position: relative; |
| } |
| |
| pre code { |
| font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; |
| font-size: 0.9rem; |
| color: #e5e7eb; |
| padding: 0; |
| background: none; |
| } |
| |
| .copy-button { |
| position: absolute; |
| top: 0.5rem; |
| right: 0.5rem; |
| padding: 0.25rem 0.5rem; |
| background-color: rgba(255, 255, 255, 0.1); |
| color: rgba(255, 255, 255, 0.6); |
| border: none; |
| border-radius: var(--border-radius-sm); |
| font-size: 0.75rem; |
| cursor: pointer; |
| transition: var(--transition-base); |
| display: flex; |
| align-items: center; |
| gap: 0.25rem; |
| } |
| |
| .copy-button:hover { |
| background-color: rgba(255, 255, 255, 0.2); |
| color: rgba(255, 255, 255, 0.9); |
| } |
| |
| |
| .code-result { |
| margin-top: 0.5rem; |
| padding: 0.75rem; |
| background-color: var(--neutral-900); |
| border-radius: var(--border-radius); |
| color: white; |
| font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; |
| font-size: 0.85rem; |
| white-space: pre-wrap; |
| } |
| |
| |
| code:not(pre code) { |
| font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; |
| font-size: 0.9em; |
| color: var(--primary-color); |
| background-color: var(--neutral-100); |
| padding: 0.2em 0.4em; |
| border-radius: 3px; |
| } |
| |
| |
| .reference-container { |
| margin-top: 1rem; |
| border-top: 1px dashed var(--neutral-300); |
| padding-top: 0.75rem; |
| } |
| |
| .reference-toggle { |
| color: var(--neutral-600); |
| font-size: 0.85rem; |
| font-weight: 500; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| transition: var(--transition-base); |
| } |
| |
| .reference-toggle:hover { |
| color: var(--secondary-color); |
| } |
| |
| .reference-toggle i { |
| margin-right: 0.35rem; |
| font-size: 0.9rem; |
| } |
| |
| .reference-content { |
| display: none; |
| margin-top: 0.75rem; |
| padding: 0.75rem; |
| background-color: var(--neutral-100); |
| border-radius: var(--border-radius); |
| font-size: 0.85rem; |
| } |
| |
| .reference-item { |
| margin-bottom: 0.75rem; |
| padding-bottom: 0.75rem; |
| border-bottom: 1px solid var(--neutral-200); |
| } |
| |
| .reference-item:last-child { |
| margin-bottom: 0; |
| padding-bottom: 0; |
| border-bottom: none; |
| } |
| |
| .reference-item-source { |
| color: var(--neutral-600); |
| font-size: 0.8rem; |
| margin-top: 0.25rem; |
| } |
| |
| |
| .welcome-message { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| text-align: center; |
| margin-bottom: 1.5rem; |
| } |
| |
| .welcome-icon { |
| width: 64px; |
| height: 64px; |
| background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light)); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin-bottom: 1rem; |
| color: white; |
| font-size: 2rem; |
| box-shadow: 0 4px 12px rgba(74, 108, 253, 0.25); |
| } |
| |
| .welcome-title { |
| font-size: 1.5rem; |
| font-weight: 600; |
| margin-bottom: 0.5rem; |
| color: var(--primary-color); |
| } |
| |
| .welcome-description { |
| color: var(--neutral-700); |
| max-width: 400px; |
| margin-bottom: 0; |
| } |
| |
| |
| .visualization-result, .mindmap-result { |
| padding: 1.5rem; |
| text-align: center; |
| } |
| |
| .visualization-image, .mindmap-image { |
| max-width: 100%; |
| border-radius: var(--border-radius); |
| box-shadow: var(--shadow); |
| } |
| |
| |
| @media (max-width: 1200px) { |
| .main-container.with-plugin .chat-container { |
| max-width: 320px; |
| } |
| } |
| |
| @media (max-width: 992px) { |
| .main-container { |
| flex-direction: column; |
| padding: 1rem; |
| } |
| |
| .main-container.with-plugin .chat-container { |
| max-width: 100%; |
| margin-bottom: 1rem; |
| height: 40vh; |
| } |
| |
| .plugin-container { |
| margin-left: 0; |
| height: calc(60vh - 32px); |
| } |
| } |
| |
| @media (max-width: 768px) { |
| .main-container { |
| padding: 0.75rem; |
| } |
| |
| .message { |
| max-width: 95%; |
| } |
| } |
| |
| |
| .katex { |
| font-size: 1.1em; |
| } |
| |
| .message h1, .message h2, .message h3, |
| .message h4, .message h5, .message h6 { |
| margin-top: 1em; |
| margin-bottom: 0.5em; |
| line-height: 1.3; |
| } |
| |
| .message h1 { |
| font-size: 1.6em; |
| border-bottom: 1px solid var(--neutral-200); |
| padding-bottom: 0.3em; |
| } |
| |
| .message h2 { |
| font-size: 1.4em; |
| border-bottom: 1px solid var(--neutral-200); |
| padding-bottom: 0.3em; |
| } |
| |
| .message h3 { |
| font-size: 1.2em; |
| } |
| |
| .message h4 { |
| font-size: 1.1em; |
| } |
| |
| .message h5, .message h6 { |
| font-size: 1em; |
| } |
| |
| .message p { |
| margin: 0.5em 0; |
| } |
| |
| .message ul, .message ol { |
| margin: 0.5em 0; |
| padding-left: 1.5em; |
| } |
| |
| .message li { |
| margin: 0.25em 0; |
| } |
| |
| .message blockquote { |
| margin: 0.5em 0; |
| padding-left: 1em; |
| border-left: 4px solid var(--neutral-300); |
| color: var(--neutral-700); |
| } |
| |
| .message img { |
| max-width: 100%; |
| border-radius: var(--border-radius); |
| } |
| |
| .message table { |
| border-collapse: collapse; |
| margin: 1em 0; |
| width: 100%; |
| } |
| |
| .message table th, |
| .message table td { |
| border: 1px solid var(--neutral-300); |
| padding: 0.5em; |
| } |
| |
| .message table th { |
| background-color: var(--neutral-100); |
| font-weight: 600; |
| } |
| |
| .message table tr:nth-child(even) { |
| background-color: var(--neutral-50); |
| } |
| |
| |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .message { |
| animation: fadeIn 0.3s ease forwards; |
| } |
| |
| |
| .input-suggestions { |
| position: absolute; |
| bottom: 100%; |
| left: 0; |
| right: 0; |
| background-color: white; |
| border-top-left-radius: var(--border-radius-lg); |
| border-top-right-radius: var(--border-radius-lg); |
| box-shadow: var(--shadow-md); |
| padding: 0.75rem; |
| display: none; |
| border: 1px solid var(--neutral-200); |
| border-bottom: none; |
| } |
| |
| .suggestion-title { |
| font-size: 0.85rem; |
| font-weight: 600; |
| color: var(--neutral-700); |
| margin-bottom: 0.5rem; |
| } |
| |
| .suggestion-buttons { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 0.5rem; |
| } |
| |
| .suggestion-button { |
| padding: 0.5rem 0.75rem; |
| background-color: var(--neutral-100); |
| border: 1px solid var(--neutral-200); |
| border-radius: var(--border-radius); |
| font-size: 0.85rem; |
| color: var(--neutral-800); |
| cursor: pointer; |
| transition: var(--transition-base); |
| } |
| |
| .suggestion-button:hover { |
| background-color: var(--secondary-light); |
| color: white; |
| border-color: var(--secondary-light); |
| } |
| |
| |
| .typing-indicator { |
| display: inline-block; |
| margin-left: 5px; |
| } |
| |
| .typing-dot { |
| display: inline-block; |
| width: 6px; |
| height: 6px; |
| border-radius: 50%; |
| background-color: var(--neutral-500); |
| margin-right: 3px; |
| animation: typingDot 1.4s infinite ease-in-out; |
| } |
| |
| .typing-dot:nth-child(1) { animation-delay: 0s; } |
| .typing-dot:nth-child(2) { animation-delay: 0.2s; } |
| .typing-dot:nth-child(3) { animation-delay: 0.4s; } |
| |
| @keyframes typingDot { |
| 0%, 60%, 100% { transform: translateY(0); } |
| 30% { transform: translateY(-5px); } |
| } |
| |
| |
| .typewriter-controls { |
| display: flex; |
| gap: 8px; |
| margin-top: 12px; |
| align-items: center; |
| justify-content: flex-end; |
| } |
| |
| .typewriter-btn { |
| width: 32px; |
| height: 32px; |
| border-radius: 50%; |
| border: none; |
| background-color: var(--neutral-100); |
| color: var(--neutral-600); |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| transition: var(--transition-base); |
| } |
| |
| .typewriter-btn:hover { |
| background-color: var(--neutral-200); |
| color: var(--neutral-900); |
| } |
| |
| .typewriter-btn.pause-btn { |
| color: var(--danger-color); |
| } |
| |
| .typewriter-btn.continue-btn { |
| color: var(--success-color); |
| } |
| |
| .typewriter-btn.speed-btn { |
| color: var(--warning-color); |
| } |
| |
| |
| .typing-cursor { |
| display: inline-block; |
| width: 2px; |
| height: 1em; |
| background-color: var(--neutral-700); |
| margin-left: 2px; |
| vertical-align: middle; |
| animation: blink 1s infinite; |
| } |
| |
| @keyframes blink { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0; } |
| } |
| |
| |
| pre { |
| position: relative; |
| white-space: pre; |
| word-wrap: normal; |
| overflow-x: auto; |
| } |
| |
| |
| .message-content p { |
| word-break: break-word; |
| } |
| |
| |
| .copy-button { |
| opacity: 0.8; |
| z-index: 10; |
| } |
| </style> |
| </head> |
| <body> |
| <header class="header"> |
| <div class="header-content"> |
| <h1><i class="bi bi-mortarboard"></i> {{ agent_name }}</h1> |
| <div> |
| <span class="badge">AI学习助手</span> |
| </div> |
| </div> |
| </header> |
| |
| <div class="main-container" id="main-container"> |
| <div class="chat-container"> |
| <div class="chat-messages" id="chat-messages"> |
| <div class="welcome-message"> |
| <div class="welcome-icon"> |
| <i class="bi bi-robot"></i> |
| </div> |
| <h2 class="welcome-title">欢迎使用 {{ agent_name }}</h2> |
| {% if agent_description %} |
| <p class="welcome-description">{{ agent_description }}</p> |
| {% else %} |
| <p class="welcome-description">我是您的AI学习助手,有任何问题都可以随时向我提问</p> |
| {% endif %} |
| </div> |
| |
| <div class="message bot-message"> |
| <div class="message-content"> |
| <p>您好!我是{{ agent_name }},很高兴能够帮助您学习。请问有什么我可以协助您的问题吗?</p> |
| </div> |
| </div> |
| </div> |
| |
| <div class="input-container"> |
| <div class="input-suggestions" id="input-suggestions"> |
| <div class="suggestion-title">推荐问题:</div> |
| <div class="suggestion-buttons"> |
| <button class="suggestion-button">介绍一下这门课程的主要内容</button> |
| <button class="suggestion-button">这门课程有哪些重点知识?</button> |
| <button class="suggestion-button">请给我一些学习建议</button> |
| </div> |
| </div> |
| <div class="input-row"> |
| <textarea class="input-field" id="user-input" placeholder="输入您的问题..." rows="2"></textarea> |
| <button class="send-button" id="send-button"> |
| <i class="bi bi-send"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="plugin-container code-plugin" id="code-plugin"> |
| <div class="plugin-header"> |
| <h3 class="plugin-title"><i class="bi bi-code-square"></i> Python代码执行</h3> |
| <button class="plugin-close" id="close-code-plugin"> |
| <i class="bi bi-x-lg"></i> |
| </button> |
| </div> |
| <div class="plugin-content"> |
| <iframe id="code-execution-frame" src="" style="width: 100%; height: 100%; border: none;"></iframe> |
| </div> |
| </div> |
| |
| |
| <div class="plugin-container visualization-plugin" id="visualization-plugin"> |
| <div class="plugin-header"> |
| <h3 class="plugin-title"><i class="bi bi-graph-up"></i> 3D可视化</h3> |
| <button class="plugin-close" id="close-visualization-plugin"> |
| <i class="bi bi-x-lg"></i> |
| </button> |
| </div> |
| <div class="plugin-content"> |
| <div class="visualization-result" id="visualization-result"> |
| <div class="text-center py-4"> |
| <div class="spinner-border text-primary" role="status"> |
| <span class="visually-hidden">加载中...</span> |
| </div> |
| <p class="mt-3">正在准备3D可视化...</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="plugin-container mindmap-plugin" id="mindmap-plugin"> |
| <div class="plugin-header"> |
| <h3 class="plugin-title"><i class="bi bi-diagram-3"></i> 思维导图</h3> |
| <button class="plugin-close" id="close-mindmap-plugin"> |
| <i class="bi bi-x-lg"></i> |
| </button> |
| </div> |
| <div class="plugin-content"> |
| <div class="mindmap-result" id="mindmap-result"> |
| <div class="text-center py-4"> |
| <div class="spinner-border text-primary" role="status"> |
| <span class="visually-hidden">加载中...</span> |
| </div> |
| <p class="mt-3">正在生成思维导图...</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/auto-render.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script> |
| <script> |
| |
| const agentId = "{{ agent_id }}"; |
| const token = "{{ token }}"; |
| let executionContext = null; |
| const mainContainer = document.getElementById('main-container'); |
| let isTyping = false; |
| let typewriterInterval = null; |
| let typewriterPaused = false; |
| let currentMessageQueue = []; |
| let typewriterSpeed = 20; |
| let currentMessageElement = null; |
| |
| |
| const chatMessages = document.getElementById('chat-messages'); |
| const userInput = document.getElementById('user-input'); |
| const sendButton = document.getElementById('send-button'); |
| |
| |
| const codePlugin = document.getElementById('code-plugin'); |
| const closeCodePlugin = document.getElementById('close-code-plugin'); |
| const codeExecutionFrame = document.getElementById('code-execution-frame'); |
| |
| |
| const visualizationPlugin = document.getElementById('visualization-plugin'); |
| const closeVisualizationPlugin = document.getElementById('close-visualization-plugin'); |
| const visualizationResult = document.getElementById('visualization-result'); |
| |
| |
| const mindmapPlugin = document.getElementById('mindmap-plugin'); |
| const closeMindmapPlugin = document.getElementById('close-mindmap-plugin'); |
| const mindmapResult = document.getElementById('mindmap-result'); |
| |
| |
| const inputSuggestions = document.getElementById('input-suggestions'); |
| |
| |
| const md = window.markdownit({ |
| html: false, |
| linkify: true, |
| typographer: true, |
| breaks: true, |
| highlight: function (str, lang) { |
| if (lang && hljs.getLanguage(lang)) { |
| try { |
| const highlighted = hljs.highlight(str, { language: lang }).value; |
| return `<pre><code class="hljs language-${lang}">${highlighted}</code><button class="copy-button" onclick="copyToClipboard(this)"><i class="bi bi-clipboard"></i> 复制</button></pre>`; |
| } catch (__) {} |
| } |
| return `<pre><code class="hljs">${md.utils.escapeHtml(str)}</code><button class="copy-button" onclick="copyToClipboard(this)"><i class="bi bi-clipboard"></i> 复制</button></pre>`; |
| } |
| }); |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| sendButton.addEventListener('click', sendMessage); |
| |
| |
| userInput.addEventListener('keydown', function(e) { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault(); |
| sendMessage(); |
| } |
| }); |
| |
| |
| document.querySelectorAll('.suggestion-button').forEach(button => { |
| button.addEventListener('click', function() { |
| userInput.value = this.textContent; |
| sendMessage(); |
| }); |
| }); |
| |
| |
| closeCodePlugin.addEventListener('click', () => { |
| codePlugin.style.display = 'none'; |
| |
| codeExecutionFrame.src = ''; |
| updateMainContainerLayout(); |
| }); |
| |
| closeVisualizationPlugin.addEventListener('click', () => { |
| visualizationPlugin.style.display = 'none'; |
| updateMainContainerLayout(); |
| }); |
| |
| closeMindmapPlugin.addEventListener('click', () => { |
| mindmapPlugin.style.display = 'none'; |
| updateMainContainerLayout(); |
| }); |
| |
| |
| userInput.addEventListener('focus', function() { |
| |
| }); |
| |
| userInput.addEventListener('blur', function(e) { |
| |
| setTimeout(() => { |
| |
| }, 100); |
| }); |
| }); |
| |
| |
| function updateMainContainerLayout() { |
| |
| const isAnyPluginVisible = |
| codePlugin.style.display === 'flex' || |
| visualizationPlugin.style.display === 'flex' || |
| mindmapPlugin.style.display === 'flex'; |
| |
| |
| if (isAnyPluginVisible) { |
| mainContainer.classList.add('with-plugin'); |
| } else { |
| mainContainer.classList.remove('with-plugin'); |
| } |
| } |
| |
| |
| async function sendMessage() { |
| const message = userInput.value.trim(); |
| if (!message || isTyping) return; |
| |
| |
| addMessage(message, true); |
| |
| |
| userInput.value = ''; |
| |
| |
| sendButton.disabled = true; |
| isTyping = true; |
| |
| |
| const typingIndicator = addTypingIndicator(); |
| |
| try { |
| |
| const response = await fetch(`/api/student/chat/${agentId}`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| message: message, |
| token: token |
| }) |
| }); |
| |
| const data = await response.json(); |
| |
| |
| if (typingIndicator) { |
| typingIndicator.remove(); |
| } |
| |
| if (data.success) { |
| |
| const processedContent = processResponseContent(data.message); |
| |
| |
| const messageElement = addMessage(processedContent, false); |
| |
| |
| if (data.references && data.references.length > 0) { |
| |
| const checkTypewriterComplete = setInterval(() => { |
| if (!typewriterInterval) { |
| clearInterval(checkTypewriterComplete); |
| addReferences(messageElement, data.references); |
| } |
| }, 100); |
| } |
| |
| |
| if (data.tools && data.tools.length > 0) { |
| |
| hideAllPlugins(); |
| |
| |
| activatePlugins(data.message, data.tools); |
| } |
| } else { |
| |
| addMessage(`错误: ${data.message}`, false); |
| } |
| } catch (error) { |
| console.error('发送请求出错:', error); |
| |
| if (typingIndicator) { |
| typingIndicator.remove(); |
| } |
| addMessage('发送请求时出错,请重试', false); |
| } finally { |
| |
| sendButton.disabled = false; |
| isTyping = false; |
| } |
| } |
| |
| |
| function addTypingIndicator() { |
| const botMessage = document.createElement('div'); |
| botMessage.className = 'message bot-message'; |
| |
| const content = document.createElement('div'); |
| content.className = 'message-content'; |
| content.innerHTML = ` |
| <p> |
| <span class="typing-indicator"> |
| <span class="typing-dot"></span> |
| <span class="typing-dot"></span> |
| <span class="typing-dot"></span> |
| </span> |
| </p> |
| `; |
| |
| botMessage.appendChild(content); |
| chatMessages.appendChild(botMessage); |
| |
| |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| |
| return botMessage; |
| } |
| |
| |
| function processResponseContent(content) { |
| |
| return content.replace(/===参考来源开始===[\s\S]*?===参考来源结束===/, ''); |
| } |
| |
| |
| function addReferences(messageElement, references) { |
| |
| const referenceContainer = document.createElement('div'); |
| referenceContainer.className = 'reference-container'; |
| |
| |
| const toggleButton = document.createElement('div'); |
| toggleButton.className = 'reference-toggle'; |
| toggleButton.innerHTML = '<i class="bi bi-journal-text"></i> 参考来源'; |
| |
| |
| const referenceContent = document.createElement('div'); |
| referenceContent.className = 'reference-content'; |
| |
| |
| references.forEach(ref => { |
| const refItem = document.createElement('div'); |
| refItem.className = 'reference-item'; |
| |
| refItem.innerHTML = ` |
| <div><strong>[${ref.index}]</strong> ${ref.summary}</div> |
| <div class="reference-item-source">来源: ${ref.file_name}</div> |
| `; |
| |
| referenceContent.appendChild(refItem); |
| }); |
| |
| |
| toggleButton.addEventListener('click', () => { |
| if (referenceContent.style.display === 'block') { |
| referenceContent.style.display = 'none'; |
| toggleButton.innerHTML = '<i class="bi bi-journal-text"></i> 参考来源'; |
| } else { |
| referenceContent.style.display = 'block'; |
| toggleButton.innerHTML = '<i class="bi bi-journal-arrow-up"></i> 收起参考来源'; |
| } |
| }); |
| |
| |
| referenceContainer.appendChild(toggleButton); |
| referenceContainer.appendChild(referenceContent); |
| messageElement.appendChild(referenceContainer); |
| } |
| |
| |
| function addMessage(content, isUser) { |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = isUser ? 'message user-message' : 'message bot-message'; |
| |
| const messageContent = document.createElement('div'); |
| messageContent.className = 'message-content'; |
| |
| if (isUser) { |
| |
| messageContent.innerHTML = `<p>${content.replace(/\n/g, '<br>')}</p>`; |
| } else { |
| |
| messageContent.innerHTML = '<p></p>'; |
| |
| const controlsDiv = document.createElement('div'); |
| controlsDiv.className = 'typewriter-controls'; |
| controlsDiv.innerHTML = ` |
| <button class="typewriter-btn pause-btn" title="暂停"><i class="bi bi-pause-fill"></i></button> |
| <button class="typewriter-btn continue-btn" style="display:none;" title="继续"><i class="bi bi-play-fill"></i></button> |
| <button class="typewriter-btn speed-btn" title="加速"><i class="bi bi-lightning-fill"></i></button> |
| `; |
| messageContent.appendChild(controlsDiv); |
| |
| |
| const pauseBtn = controlsDiv.querySelector('.pause-btn'); |
| const continueBtn = controlsDiv.querySelector('.continue-btn'); |
| const speedBtn = controlsDiv.querySelector('.speed-btn'); |
| |
| pauseBtn.addEventListener('click', () => { |
| pauseTypewriter(); |
| pauseBtn.style.display = 'none'; |
| continueBtn.style.display = 'inline-block'; |
| }); |
| |
| continueBtn.addEventListener('click', () => { |
| continueTypewriter(); |
| continueBtn.style.display = 'none'; |
| pauseBtn.style.display = 'inline-block'; |
| }); |
| |
| speedBtn.addEventListener('click', () => { |
| toggleTypewriterSpeed(); |
| }); |
| |
| |
| startTypewriter(content, messageContent.querySelector('p')); |
| } |
| |
| messageDiv.appendChild(messageContent); |
| chatMessages.appendChild(messageDiv); |
| |
| |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| |
| return messageDiv; |
| } |
| |
| |
| function startTypewriter(text, element) { |
| |
| if (typewriterInterval) { |
| clearInterval(typewriterInterval); |
| } |
| |
| |
| currentMessageElement = element; |
| |
| |
| typewriterPaused = false; |
| let mdContent = md.render(text); |
| |
| |
| parseHTMLToQueue(mdContent); |
| |
| |
| typewriterInterval = setInterval(typeNextChar, typewriterSpeed); |
| } |
| |
| |
| function parseHTMLToQueue(html) { |
| |
| currentMessageQueue = []; |
| |
| |
| const tempDiv = document.createElement('div'); |
| tempDiv.innerHTML = html; |
| |
| |
| processNode(tempDiv); |
| } |
| |
| |
| function processNode(node) { |
| |
| for (let i = 0; i < node.childNodes.length; i++) { |
| const child = node.childNodes[i]; |
| |
| if (child.nodeType === Node.TEXT_NODE) { |
| |
| for (let j = 0; j < child.textContent.length; j++) { |
| currentMessageQueue.push({ |
| type: 'text', |
| content: child.textContent[j] |
| }); |
| } |
| } else if (child.nodeType === Node.ELEMENT_NODE) { |
| |
| currentMessageQueue.push({ |
| type: 'element-start', |
| tagName: child.tagName.toLowerCase(), |
| attributes: Array.from(child.attributes) |
| }); |
| |
| |
| processNode(child); |
| |
| |
| currentMessageQueue.push({ |
| type: 'element-end', |
| tagName: child.tagName.toLowerCase() |
| }); |
| } |
| } |
| } |
| |
| |
| function typeNextChar() { |
| if (typewriterPaused || currentMessageQueue.length === 0 || !currentMessageElement) { |
| return; |
| } |
| |
| let item = currentMessageQueue.shift(); |
| |
| if (item.type === 'text') { |
| |
| currentMessageElement.innerHTML += item.content; |
| } else if (item.type === 'element-start') { |
| |
| const el = document.createElement(item.tagName); |
| if (item.attributes) { |
| item.attributes.forEach(attr => { |
| el.setAttribute(attr.name, attr.value); |
| }); |
| } |
| currentMessageElement.appendChild(el); |
| |
| currentMessageElement = el; |
| } else if (item.type === 'element-end') { |
| |
| |
| if (currentMessageElement.parentElement) { |
| currentMessageElement = currentMessageElement.parentElement; |
| } |
| } |
| |
| |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| |
| |
| if (currentMessageQueue.length === 0) { |
| clearInterval(typewriterInterval); |
| typewriterInterval = null; |
| |
| |
| const botMessage = chatMessages.querySelector('.bot-message:last-child'); |
| if (botMessage) { |
| try { |
| renderMathInElement(botMessage.querySelector('.message-content'), { |
| delimiters: [ |
| {left: '$$', right: '$$', display: true}, |
| {left: '$', right: '$', display: false}, |
| {left: '\\(', right: '\\)', display: false}, |
| {left: '\\[', right: '\\]', display: true} |
| ], |
| throwOnError: false |
| }); |
| } catch (e) { |
| console.error('渲染LaTeX出错:', e); |
| } |
| |
| |
| botMessage.querySelectorAll('pre').forEach(pre => { |
| if (!pre.querySelector('.copy-button')) { |
| const copyButton = document.createElement('button'); |
| copyButton.className = 'copy-button'; |
| copyButton.innerHTML = '<i class="bi bi-clipboard"></i> 复制'; |
| copyButton.onclick = function() { copyToClipboard(this); }; |
| pre.appendChild(copyButton); |
| } |
| }); |
| } |
| } |
| } |
| |
| |
| function pauseTypewriter() { |
| typewriterPaused = true; |
| } |
| |
| |
| function continueTypewriter() { |
| typewriterPaused = false; |
| |
| |
| if (!typewriterInterval && currentMessageQueue.length > 0) { |
| typewriterInterval = setInterval(typeNextChar, typewriterSpeed); |
| } |
| } |
| |
| |
| function toggleTypewriterSpeed() { |
| if (typewriterSpeed === 20) { |
| typewriterSpeed = 5; |
| } else { |
| typewriterSpeed = 20; |
| } |
| |
| |
| if (typewriterInterval) { |
| clearInterval(typewriterInterval); |
| typewriterInterval = setInterval(typeNextChar, typewriterSpeed); |
| } |
| } |
| |
| |
| function copyToClipboard(button) { |
| const codeBlock = button.parentNode.querySelector('code'); |
| const text = codeBlock.textContent; |
| |
| navigator.clipboard.writeText(text).then(() => { |
| button.innerHTML = '<i class="bi bi-check"></i> 已复制'; |
| |
| setTimeout(() => { |
| button.innerHTML = '<i class="bi bi-clipboard"></i> 复制'; |
| }, 2000); |
| }).catch(err => { |
| console.error('复制失败:', err); |
| button.innerHTML = '<i class="bi bi-exclamation-triangle"></i> 失败'; |
| |
| setTimeout(() => { |
| button.innerHTML = '<i class="bi bi-clipboard"></i> 复制'; |
| }, 2000); |
| }); |
| } |
| |
| |
| function hideAllPlugins() { |
| codePlugin.style.display = 'none'; |
| visualizationPlugin.style.display = 'none'; |
| mindmapPlugin.style.display = 'none'; |
| updateMainContainerLayout(); |
| } |
| |
| |
| function extractCodeBlocks(message) { |
| const codeBlocks = []; |
| const codeRegex = /```python\n([\s\S]*?)\n```/g; |
| |
| let match; |
| while ((match = codeRegex.exec(message)) !== null) { |
| codeBlocks.push(match[1]); |
| } |
| |
| return codeBlocks; |
| } |
| |
| |
| function extract3DVisualizationCode(message) { |
| |
| const codeRegex = /```python\s*(import[\s\S]*?def create_3d_plot\(\):[\s\S]*?return[\s\S]*?})\s*```/i; |
| const match = codeRegex.exec(message); |
| |
| if (match) { |
| return match[1].trim(); |
| } |
| |
| return null; |
| } |
| |
| |
| function extractMindmapContent(message) { |
| |
| const mindmapRegex = /@startmindmap\n([\s\S]*?)@endmindmap/; |
| const match = mindmapRegex.exec(message); |
| |
| if (match) { |
| return `@startmindmap\n${match[1]}\n@endmindmap`; |
| } |
| |
| return null; |
| } |
| |
| |
| function activateCodePlugin(message) { |
| |
| const codeBlocks = extractCodeBlocks(message); |
| |
| if (codeBlocks.length > 0) { |
| const isAlreadyVisible = codePlugin.style.display === 'flex'; |
| |
| |
| codePlugin.style.display = 'flex'; |
| updateMainContainerLayout(); |
| |
| |
| const iframe = document.getElementById('code-execution-frame'); |
| |
| |
| if (isAlreadyVisible && iframe.contentWindow) { |
| |
| iframe.contentWindow.postMessage({ |
| type: 'setCode', |
| code: codeBlocks[0] |
| }, '*'); |
| } else { |
| |
| let src = '/code_execution.html'; |
| if (codeBlocks.length > 0) { |
| src += `?code=${encodeURIComponent(codeBlocks[0])}`; |
| } |
| |
| iframe.src = src; |
| |
| |
| iframe.onload = function() { |
| if (codeBlocks.length > 0) { |
| iframe.contentWindow.postMessage({ |
| type: 'setCode', |
| code: codeBlocks[0] |
| }, '*'); |
| } |
| }; |
| } |
| } |
| } |
| |
| |
| function activate3DVisualization(message) { |
| const code = extract3DVisualizationCode(message); |
| |
| if (code) { |
| |
| visualizationPlugin.style.display = 'flex'; |
| updateMainContainerLayout(); |
| |
| |
| visualizationResult.innerHTML = ` |
| <div class="text-center py-4"> |
| <div class="spinner-border" style="color: var(--secondary-color);" role="status"> |
| <span class="visually-hidden">生成中...</span> |
| </div> |
| <p class="mt-3">正在生成3D图形,请稍候...</p> |
| </div> |
| `; |
| |
| |
| fetch('/api/visualization/3d-surface', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ code: code }) |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| |
| visualizationResult.innerHTML = ` |
| <div class="visualization-iframe-container" style="width:100%; height:500px;"> |
| <iframe src="${data.html_url}" style="width:100%; height:100%; border:none;"></iframe> |
| </div> |
| <div class="text-center mt-3"> |
| <p>3D图形生成成功</p> |
| <a href="${data.html_url}" class="btn btn-sm btn-outline-primary" target="_blank"> |
| <i class="bi bi-arrows-fullscreen"></i> 全屏查看 |
| </a> |
| </div> |
| `; |
| } else { |
| visualizationResult.innerHTML = ` |
| <div class="alert alert-danger"> |
| <i class="bi bi-exclamation-triangle-fill me-2"></i> |
| 生成失败: ${data.message} |
| </div> |
| `; |
| } |
| }) |
| .catch(error => { |
| console.error('生成3D图形出错:', error); |
| visualizationResult.innerHTML = ` |
| <div class="alert alert-danger"> |
| <i class="bi bi-exclamation-triangle-fill me-2"></i> |
| 生成3D图形时发生错误,请重试 |
| </div> |
| `; |
| }); |
| } |
| } |
| |
| |
| function activateMindmap(message) { |
| const content = extractMindmapContent(message); |
| |
| if (content) { |
| |
| mindmapPlugin.style.display = 'flex'; |
| updateMainContainerLayout(); |
| |
| |
| mindmapResult.innerHTML = ` |
| <div class="text-center py-4"> |
| <div class="spinner-border" style="color: var(--secondary-color);" role="status"> |
| <span class="visually-hidden">生成中...</span> |
| </div> |
| <p class="mt-3">正在生成思维导图,请稍候...</p> |
| </div> |
| `; |
| |
| |
| fetch('/api/visualization/mindmap', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ content: content }) |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data.success) { |
| mindmapResult.innerHTML = ` |
| <div class="text-center"> |
| <img src="${data.url}" class="mindmap-image" alt="思维导图"> |
| <p class="mt-3">生成成功!</p> |
| <a href="${data.url}" class="btn btn-sm btn-outline-primary mt-2" target="_blank"> |
| <i class="bi bi-download"></i> 下载图片 |
| </a> |
| </div> |
| `; |
| } else { |
| mindmapResult.innerHTML = ` |
| <div class="alert alert-danger"> |
| <i class="bi bi-exclamation-triangle-fill me-2"></i> |
| 生成失败: ${data.message} |
| </div> |
| `; |
| } |
| }) |
| .catch(error => { |
| console.error('生成思维导图出错:', error); |
| mindmapResult.innerHTML = ` |
| <div class="alert alert-danger"> |
| <i class="bi bi-exclamation-triangle-fill me-2"></i> |
| 生成思维导图时发生错误,请重试 |
| </div> |
| `; |
| }); |
| } |
| } |
| |
| |
| function activatePlugins(message, tools) { |
| |
| if (tools.includes('code') && message.includes('```python')) { |
| activateCodePlugin(message); |
| } |
| |
| |
| if (tools.includes('visualization') && |
| (message.includes('def create_3d_plot') || |
| message.includes('3D') || message.includes('可视化'))) { |
| activate3DVisualization(message); |
| } |
| |
| |
| if (tools.includes('mindmap') && |
| (message.includes('@startmindmap') || |
| message.includes('思维导图'))) { |
| activateMindmap(message); |
| } |
| } |
| </script> |
| |
| |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script> |
| </body> |
| </html> |