Download index.html from 3VVM/GLLM2: direct link, hf CLI and curl.
- Browser
- Download file 85.2 kB
-
https://huggingface.co/spaces/3VVM/GLLM2/resolve/main/index.html
- Command line
-
hf download hf://spaces/3VVM/GLLM2/index.html
-
curl -L -o index.html https://huggingface.co/spaces/3VVM/GLLM2/resolve/main/index.html
85.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GLM 5.2</title> | |
| <!-- Google 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=Fira+Code:wght@400;500;600&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- Markdown Parser --> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <style> | |
| :root { | |
| /* Clean Minimalist Light Mode (Screenshot Aesthetics) */ | |
| --bg-color: #ffffff; | |
| --sidebar-bg: #ffffff; | |
| --sidebar-border: rgba(0, 0, 0, 0.06); | |
| --sidebar-icon-color: #71717a; | |
| --sidebar-icon-hover: #18181b; | |
| --sidebar-active-bg: #f4f4f5; | |
| --text-color: #09090b; | |
| --text-muted: #71717a; | |
| --text-terminal: #27272a; | |
| --input-bg: #ffffff; | |
| --input-border: rgba(0, 0, 0, 0.08); | |
| --input-shadow: 0 4px 20px rgba(0, 0, 0, 0.03), 0 10px 40px -10px rgba(0, 0, 0, 0.05); | |
| --btn-active-bg: #18181b; | |
| --btn-active-color: #ffffff; | |
| --btn-disabled-bg: #f4f4f5; | |
| --btn-disabled-color: #a1a1aa; | |
| --font-serif: 'Playfair Display', serif; | |
| --font-sans: 'Outfit', 'Inter', sans-serif; | |
| --font-mono: 'Fira Code', monospace; | |
| --msg-user-bg: transparent; | |
| --msg-assistant-bg: transparent; | |
| --code-bg: #18181b; | |
| --code-color: #f4f4f5; | |
| --accent-color: #18181b; | |
| --accent-bg: #f4f4f5; | |
| --watermark-color: rgba(0, 0, 0, 0.02); | |
| --capsule-bg: #f4f4f5; | |
| --capsule-text: #27272a; | |
| --capsule-border: rgba(0, 0, 0, 0.04); | |
| --cursor-color: #18181b; | |
| } | |
| /* Dark Modern Mode */ | |
| .dark-mode { | |
| --bg-color: #09090b; | |
| --sidebar-bg: #09090b; | |
| --sidebar-border: rgba(255, 255, 255, 0.06); | |
| --sidebar-icon-color: #a1a1aa; | |
| --sidebar-icon-hover: #ffffff; | |
| --sidebar-active-bg: #27272a; | |
| --text-color: #f4f4f5; | |
| --text-muted: #a1a1aa; | |
| --text-terminal: #e4e4e7; | |
| --input-bg: #09090b; | |
| --input-border: rgba(255, 255, 255, 0.08); | |
| --input-shadow: 0 4px 25px rgba(0, 0, 0, 0.4); | |
| --btn-active-bg: #ffffff; | |
| --btn-active-color: #09090b; | |
| --btn-disabled-bg: #27272a; | |
| --btn-disabled-color: #71717a; | |
| --code-bg: #27272a; | |
| --code-color: #f4f4f5; | |
| --accent-color: #ffffff; | |
| --accent-bg: #27272a; | |
| --watermark-color: rgba(255, 255, 255, 0.015); | |
| --capsule-bg: #27272a; | |
| --capsule-text: #e4e4e7; | |
| --capsule-border: rgba(255, 255, 255, 0.04); | |
| --cursor-color: #ffffff; | |
| } | |
| /* Classic Retro Hacker Terminal Theme */ | |
| .classic-terminal-mode { | |
| --bg-color: #050806; | |
| --sidebar-bg: #020403; | |
| --sidebar-border: rgba(51, 255, 51, 0.15); | |
| --sidebar-icon-color: rgba(51, 255, 51, 0.6); | |
| --sidebar-icon-hover: #33ff33; | |
| --sidebar-active-bg: rgba(51, 255, 51, 0.1); | |
| --text-color: #33ff33; | |
| --text-muted: rgba(51, 255, 51, 0.6); | |
| --text-terminal: #33ff33; | |
| --input-bg: #020403; | |
| --input-border: rgba(51, 255, 51, 0.3); | |
| --input-shadow: 0 0 15px rgba(51, 255, 51, 0.1); | |
| --btn-active-bg: #33ff33; | |
| --btn-active-color: #020403; | |
| --btn-disabled-bg: rgba(51, 255, 51, 0.05); | |
| --btn-disabled-color: rgba(51, 255, 51, 0.3); | |
| --font-serif: 'Fira Code', monospace; | |
| --font-sans: 'Fira Code', monospace; | |
| --font-mono: 'Fira Code', monospace; | |
| --code-bg: #020403; | |
| --code-color: #33ff33; | |
| --accent-color: #33ff33; | |
| --accent-bg: rgba(51, 255, 51, 0.08); | |
| --watermark-color: rgba(51, 255, 51, 0.02); | |
| --capsule-bg: rgba(51, 255, 51, 0.05); | |
| --capsule-text: #33ff33; | |
| --capsule-border: rgba(51, 255, 51, 0.2); | |
| --cursor-color: #33ff33; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| transition: background-color 0.25s ease, border-color 0.25s ease, color 0.15s ease; | |
| } | |
| body, html { | |
| height: 100%; | |
| width: 100%; | |
| font-family: var(--font-sans); | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| overflow: hidden; | |
| display: flex; | |
| } | |
| /* Sidebar Styling (Matches Screenshot Layout) */ | |
| .sidebar { | |
| width: 72px; | |
| height: 100%; | |
| background-color: var(--sidebar-bg); | |
| border-right: 1px solid var(--sidebar-border); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 16px 0; | |
| justify-content: space-between; | |
| z-index: 10; | |
| } | |
| .sidebar-top { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 16px; | |
| width: 100%; | |
| } | |
| .sidebar-bottom { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 16px; | |
| width: 100%; | |
| } | |
| .sidebar-btn { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 8px; | |
| border: 1px solid transparent; | |
| background: transparent; | |
| color: var(--sidebar-icon-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| position: relative; | |
| } | |
| .sidebar-btn:hover { | |
| color: var(--sidebar-icon-hover); | |
| background-color: var(--sidebar-active-bg); | |
| } | |
| .sidebar-btn.active { | |
| background-color: var(--sidebar-active-bg); | |
| color: var(--sidebar-icon-hover); | |
| border-color: var(--sidebar-border); | |
| } | |
| /* Top Brand Button (Boxed Z Logo) */ | |
| .brand-btn { | |
| width: 44px; | |
| height: 44px; | |
| border-radius: 8px; | |
| background-color: var(--bg-color); | |
| border: 1px solid var(--sidebar-border); | |
| color: var(--text-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| font-weight: 700; | |
| font-family: var(--font-sans); | |
| font-size: 18px; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02); | |
| } | |
| .brand-btn:hover { | |
| background-color: var(--sidebar-active-bg); | |
| } | |
| /* Tooltip */ | |
| .sidebar-btn::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| left: 80px; | |
| background: #18181b; | |
| color: #ffffff; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.2s ease, transform 0.2s ease; | |
| transform: translateX(-10px); | |
| z-index: 100; | |
| font-family: var(--font-sans); | |
| } | |
| .sidebar-btn:hover::after { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| /* Main Workspace */ | |
| .workspace { | |
| flex: 1; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| } | |
| /* Watermark Background Z */ | |
| .watermark-container { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 450px; | |
| height: 450px; | |
| pointer-events: none; | |
| z-index: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .watermark-svg { | |
| width: 100%; | |
| height: 100%; | |
| stroke: var(--watermark-color); | |
| fill: none; | |
| stroke-width: 0.8; | |
| stroke-linecap: round; | |
| stroke-linejoin: round; | |
| } | |
| /* Welcome Center State */ | |
| .welcome-panel { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| z-index: 2; | |
| padding: 32px; | |
| user-select: none; | |
| } | |
| .welcome-title { | |
| font-family: var(--font-serif); | |
| font-size: 48px; | |
| font-weight: 400; | |
| margin-bottom: 16px; | |
| color: var(--text-color); | |
| letter-spacing: -0.5px; | |
| animation: fadeInUp 0.8s ease; | |
| } | |
| .welcome-subtitle { | |
| font-family: var(--font-sans); | |
| font-size: 16px; | |
| color: var(--text-muted); | |
| font-weight: 400; | |
| letter-spacing: 0.1px; | |
| animation: fadeInUp 0.8s ease 0.1s both; | |
| } | |
| /* Chat Terminal Console Output */ | |
| .chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 40px 10%; | |
| display: none; | |
| flex-direction: column; | |
| gap: 24px; | |
| z-index: 2; | |
| scroll-behavior: smooth; | |
| } | |
| .chat-container.terminal-layout { | |
| background-size: 20px 20px; | |
| background-image: | |
| linear-gradient(to right, rgba(0, 0, 0, 0.01) 1px, transparent 1px), | |
| linear-gradient(to bottom, rgba(0, 0, 0, 0.01) 1px, transparent 1px); | |
| } | |
| .dark-mode .chat-container.terminal-layout { | |
| background-image: | |
| linear-gradient(to right, rgba(255, 255, 255, 0.005) 1px, transparent 1px), | |
| linear-gradient(to bottom, rgba(255, 255, 255, 0.005) 1px, transparent 1px); | |
| } | |
| .classic-terminal-mode .chat-container.terminal-layout { | |
| background-image: | |
| linear-gradient(to right, rgba(51, 255, 51, 0.015) 1px, transparent 1px), | |
| linear-gradient(to bottom, rgba(51, 255, 51, 0.015) 1px, transparent 1px); | |
| } | |
| .message-row { | |
| display: flex; | |
| flex-direction: column; | |
| width: 100%; | |
| animation: fadeIn 0.3s ease-out; | |
| line-height: 1.6; | |
| } | |
| /* Terminal Prompt Headers */ | |
| .prompt-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 4px; | |
| user-select: none; | |
| } | |
| .prompt-prefix-user { | |
| color: #3b82f6; /* Modern Blue */ | |
| font-weight: 600; | |
| } | |
| .classic-terminal-mode .prompt-prefix-user { | |
| color: #33ff33; | |
| } | |
| .prompt-prefix-system { | |
| color: #10b981; /* Modern Green */ | |
| font-weight: 600; | |
| } | |
| .classic-terminal-mode .prompt-prefix-system { | |
| color: #33ff33; | |
| } | |
| .prompt-command { | |
| color: var(--text-muted); | |
| font-size: 12px; | |
| } | |
| .prompt-text { | |
| color: var(--text-color); | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| padding-left: 4px; | |
| } | |
| .message-content { | |
| padding-left: 16px; | |
| border-left: 2px solid var(--sidebar-border); | |
| color: var(--text-terminal); | |
| margin-top: 4px; | |
| word-break: break-word; | |
| } | |
| .message-content p { | |
| margin-bottom: 12px; | |
| } | |
| .message-content p:last-child { | |
| margin-bottom: 0; | |
| } | |
| /* Code Block Styling */ | |
| .message-content pre { | |
| background-color: var(--code-bg); | |
| color: var(--code-color); | |
| padding: 14px; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| margin: 12px 0; | |
| border: 1px solid var(--sidebar-border); | |
| position: relative; | |
| } | |
| .message-content code { | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| } | |
| /* ===== Agent Tool Cards & File Chips ===== */ | |
| .tool-card { | |
| background-color: var(--accent-bg); | |
| border: 1px solid var(--sidebar-border); | |
| border-radius: 10px; | |
| margin: 10px 0; | |
| overflow: hidden; | |
| font-family: var(--font-mono); | |
| font-size: 12.5px; | |
| } | |
| .tool-card-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 9px 12px; | |
| color: var(--text-terminal); | |
| border-bottom: 1px solid var(--sidebar-border); | |
| } | |
| .tool-card-header .tool-name { | |
| font-weight: 600; | |
| flex-shrink: 0; | |
| } | |
| .tool-card-header .tool-args-preview { | |
| color: var(--text-muted); | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| flex: 1; | |
| } | |
| .tool-status { | |
| flex-shrink: 0; | |
| font-size: 10px; | |
| font-weight: 600; | |
| letter-spacing: 0.04em; | |
| text-transform: uppercase; | |
| padding: 2px 8px; | |
| border-radius: 999px; | |
| } | |
| .tool-status.pending { | |
| color: var(--text-muted); | |
| border: 1px solid var(--sidebar-border); | |
| } | |
| .tool-status.success { | |
| color: #0a7d34; | |
| background-color: rgba(16, 185, 90, 0.12); | |
| } | |
| .tool-status.fail { | |
| color: #c0392b; | |
| background-color: rgba(220, 60, 40, 0.1); | |
| } | |
| .tool-card-body { | |
| padding: 10px 12px; | |
| } | |
| .tool-card-output { | |
| background-color: var(--code-bg); | |
| color: var(--code-color); | |
| padding: 10px 12px; | |
| border-radius: 6px; | |
| overflow-x: auto; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| max-height: 260px; | |
| overflow-y: auto; | |
| margin: 0; | |
| } | |
| .tool-card-output.stderr-block { | |
| margin-top: 8px; | |
| border-left: 3px solid #c0392b; | |
| } | |
| .tool-card-file-line { | |
| margin-top: 8px; | |
| color: var(--text-muted); | |
| } | |
| .file-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 14px; | |
| margin: 6px 0; | |
| background-color: var(--accent-bg); | |
| border: 1px solid var(--sidebar-border); | |
| border-radius: 10px; | |
| text-decoration: none; | |
| color: var(--text-color); | |
| font-family: var(--font-sans); | |
| font-size: 13.5px; | |
| transition: background-color 0.15s ease; | |
| } | |
| .file-chip:hover { | |
| background-color: var(--sidebar-active-bg); | |
| } | |
| .file-chip svg { | |
| flex-shrink: 0; | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .file-chip .file-chip-meta { | |
| color: var(--text-muted); | |
| font-size: 11.5px; | |
| } | |
| .attach-btn { | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| color: var(--sidebar-icon-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 6px; | |
| border-radius: 8px; | |
| transition: color 0.15s ease, background-color 0.15s ease; | |
| } | |
| .attach-btn:hover { | |
| color: var(--sidebar-icon-hover); | |
| background-color: var(--sidebar-active-bg); | |
| } | |
| .attach-btn svg { | |
| width: 17px; | |
| height: 17px; | |
| } | |
| .pending-upload-row { | |
| display: none; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 10px; | |
| margin-bottom: 4px; | |
| background-color: var(--accent-bg); | |
| border-radius: 8px; | |
| font-size: 12.5px; | |
| color: var(--text-muted); | |
| font-family: var(--font-mono); | |
| } | |
| .pending-upload-row.visible { | |
| display: flex; | |
| } | |
| .pending-upload-row .remove-upload { | |
| cursor: pointer; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| margin-left: auto; | |
| } | |
| .pending-upload-row .remove-upload:hover { | |
| color: var(--text-color); | |
| } | |
| /* Floating Message Input Panel (Matches GLM-5.2 Layout) */ | |
| .input-panel { | |
| width: 100%; | |
| max-width: 800px; | |
| margin: 0 auto 32px auto; | |
| padding: 0 24px; | |
| z-index: 5; | |
| } | |
| .input-container { | |
| background-color: var(--input-bg); | |
| border: 1px solid var(--input-border); | |
| border-radius: 16px; | |
| padding: 16px 20px; | |
| box-shadow: var(--input-shadow); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .textarea-wrapper { | |
| width: 100%; | |
| } | |
| .chat-input { | |
| width: 100%; | |
| border: none; | |
| outline: none; | |
| background: transparent; | |
| color: var(--text-color); | |
| font-family: var(--font-sans); | |
| font-size: 16px; | |
| resize: none; | |
| max-height: 200px; | |
| height: 24px; | |
| line-height: 1.5; | |
| } | |
| .chat-input::placeholder { | |
| color: var(--text-muted); | |
| opacity: 0.8; | |
| } | |
| /* Input Controls Panel */ | |
| .input-controls { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| width: 100%; | |
| } | |
| .controls-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .action-circle-btn { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| border: 1px solid var(--input-border); | |
| background: transparent; | |
| color: var(--text-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| } | |
| .action-circle-btn:hover { | |
| background-color: var(--sidebar-active-bg); | |
| } | |
| /* Capsule Agent Button (Screenshot style) */ | |
| .agent-capsule { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| background-color: var(--capsule-bg); | |
| border: 1px solid var(--capsule-border); | |
| color: var(--capsule-text); | |
| padding: 6px 14px; | |
| border-radius: 20px; | |
| font-family: var(--font-sans); | |
| font-size: 13px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| user-select: none; | |
| border: 1px solid var(--sidebar-border); | |
| } | |
| .agent-capsule:hover { | |
| background-color: var(--sidebar-active-bg); | |
| filter: brightness(0.95); | |
| } | |
| .agent-capsule svg { | |
| color: var(--text-muted); | |
| } | |
| /* Send Circular Button (Screenshot style) */ | |
| .send-btn { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| border: none; | |
| background-color: var(--btn-disabled-bg); | |
| color: var(--btn-disabled-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .send-btn.active { | |
| background-color: var(--btn-active-bg); | |
| color: var(--btn-active-color); | |
| } | |
| .send-btn svg { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| /* Blinking Terminal Cursor */ | |
| .terminal-cursor { | |
| display: inline-block; | |
| width: 8px; | |
| height: 15px; | |
| background-color: var(--cursor-color); | |
| margin-left: 4px; | |
| animation: blink 0.8s infinite; | |
| vertical-align: middle; | |
| } | |
| /* Settings Drawer overlay */ | |
| .settings-drawer { | |
| position: absolute; | |
| left: 73px; | |
| top: 0; | |
| bottom: 0; | |
| width: 320px; | |
| background-color: var(--sidebar-bg); | |
| border-right: 1px solid var(--sidebar-border); | |
| z-index: 9; | |
| box-shadow: 10px 0 30px rgba(0,0,0,0.02); | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| transform: translateX(-105%); | |
| transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .settings-drawer.open { | |
| transform: translateX(0); | |
| } | |
| .settings-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .settings-title { | |
| font-size: 18px; | |
| font-weight: 600; | |
| } | |
| .close-drawer-btn { | |
| background: transparent; | |
| border: none; | |
| color: var(--text-muted); | |
| cursor: pointer; | |
| } | |
| .close-drawer-btn:hover { | |
| color: var(--text-color); | |
| } | |
| .settings-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .settings-label { | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| } | |
| .settings-input, .settings-select { | |
| width: 100%; | |
| background-color: var(--input-bg); | |
| border: 1px solid var(--sidebar-border); | |
| padding: 10px 12px; | |
| border-radius: 8px; | |
| color: var(--text-color); | |
| font-family: var(--font-sans); | |
| font-size: 14px; | |
| outline: none; | |
| } | |
| .settings-input:focus, .settings-select:focus { | |
| border-color: var(--text-color); | |
| } | |
| /* CLI Info Alert */ | |
| .cli-info-banner { | |
| background-color: var(--accent-bg); | |
| color: var(--text-color); | |
| border: 1px solid var(--sidebar-border); | |
| border-radius: 8px; | |
| padding: 12px; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| font-family: var(--font-mono); | |
| } | |
| /* Matrix Canvas */ | |
| #matrix-canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 0; | |
| opacity: 0.12; | |
| display: none; | |
| } | |
| /* Animations */ | |
| @keyframes blink { | |
| 0%, 49% { opacity: 1; } | |
| 50%, 100% { opacity: 0; } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| /* Scrollbar styling */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--sidebar-border); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-muted); | |
| } | |
| /* System notification log */ | |
| .system-log { | |
| color: #eab308; | |
| font-style: italic; | |
| } | |
| .classic-terminal-mode .system-log { | |
| color: #ffb000; | |
| } | |
| /* Welcoming terminal dashboard box */ | |
| .welcome-terminal-box { | |
| background-color: var(--input-bg); | |
| border: 1px solid var(--input-border); | |
| border-radius: 12px; | |
| width: 100%; | |
| max-width: 480px; | |
| box-shadow: var(--input-shadow); | |
| text-align: left; | |
| margin: 20px 0; | |
| overflow: hidden; | |
| animation: fadeInUp 0.8s ease 0.2s both; | |
| } | |
| .welcome-terminal-header { | |
| background-color: var(--sidebar-bg); | |
| border-bottom: 1px solid var(--sidebar-border); | |
| padding: 10px 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .terminal-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| } | |
| .terminal-dot.red { background-color: #ef4444; } | |
| .terminal-dot.yellow { background-color: #f59e0b; } | |
| .terminal-dot.green { background-color: #10b981; } | |
| .terminal-title { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| margin-left: 8px; | |
| flex-grow: 1; | |
| text-align: center; | |
| margin-right: 30px; | |
| } | |
| .welcome-terminal-content { | |
| padding: 16px 20px; | |
| margin: 0; | |
| overflow: auto; | |
| } | |
| .welcome-terminal-content code { | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| color: var(--text-terminal); | |
| line-height: 1.6; | |
| } | |
| /* Chat Status Bar styles */ | |
| .chat-status-bar { | |
| background-color: var(--sidebar-bg); | |
| border-bottom: 1px solid var(--sidebar-border); | |
| padding: 10px 24px; | |
| font-size: 12px; | |
| display: none; | |
| align-items: center; | |
| justify-content: space-between; | |
| color: var(--text-muted); | |
| z-index: 3; | |
| user-select: none; | |
| } | |
| .chat-status-bar .status-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .chat-status-bar .status-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background-color: #10b981; | |
| } | |
| /* Message styling variations based on Mode */ | |
| .message-row.style-standard { | |
| font-family: var(--font-sans); | |
| font-size: 15px; | |
| line-height: 1.6; | |
| } | |
| .message-row.style-standard .prompt-header { | |
| margin-bottom: 6px; | |
| } | |
| .message-row.style-standard .standard-sender-user { | |
| font-weight: 600; | |
| font-size: 13px; | |
| color: var(--text-color); | |
| background-color: var(--sidebar-active-bg); | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| } | |
| .message-row.style-standard .standard-sender-system { | |
| font-weight: 600; | |
| font-size: 13px; | |
| color: var(--text-color); | |
| } | |
| .message-row.style-standard .prompt-text { | |
| padding-left: 0; | |
| color: var(--text-color); | |
| font-size: 15px; | |
| } | |
| .message-row.style-standard .message-content { | |
| padding-left: 0; | |
| border-left: none; | |
| color: var(--text-color); | |
| font-size: 15px; | |
| } | |
| .message-row.style-standard .message-content p { | |
| margin-bottom: 12px; | |
| } | |
| /* Standard Mode code blocks styling */ | |
| .message-row.style-standard .message-content pre { | |
| background-color: var(--capsule-bg); | |
| color: var(--text-color); | |
| border: 1px solid var(--sidebar-border); | |
| } | |
| .message-row.style-standard .message-content code { | |
| color: inherit; | |
| } | |
| .message-row.style-terminal { | |
| font-family: var(--font-mono); | |
| font-size: 14px; | |
| line-height: 1.6; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Matrix Code Rain Canvas --> | |
| <canvas id="matrix-canvas"></canvas> | |
| <!-- Left Sidebar (Screenshots structure) --> | |
| <nav class="sidebar"> | |
| <div class="sidebar-top"> | |
| <!-- Brand GLM Logo (Minimalist Boxed Header) --> | |
| <button class="brand-btn" id="home-brand-btn" onclick="goToHome()" title="GLM 5.2 Workspace" style="font-size: 11px; font-weight: 800; letter-spacing: -0.5px;">GLM</button> | |
| <!-- Hugging Face Sign-In / Profile --> | |
| <button class="sidebar-btn" id="hf-auth-btn" onclick="handleHfAuthClick()" data-tooltip="Sign in with Hugging Face" style="font-size: 20px; line-height: 1;">🤗</button> | |
| <button class="sidebar-btn" id="new-chat-btn" onclick="clearSession()" data-tooltip="New Workspace Chat"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/> | |
| <path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="sidebar-bottom"> | |
| <!-- Theme Toggler (Sun/Moon/Retro CLI Terminal) --> | |
| <button class="sidebar-btn" id="theme-btn" onclick="cycleTheme()" data-tooltip="Cycle Theme Mode"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/> | |
| <circle cx="12" cy="12" r="4"/> | |
| </svg> | |
| </button> | |
| <!-- Configuration / Settings Cog --> | |
| <button class="sidebar-btn" id="settings-btn" onclick="toggleSettings()" data-tooltip="Settings & Credentials"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="3"/> | |
| <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </nav> | |
| <!-- Sliding Settings Drawer --> | |
| <div class="settings-drawer" id="settings-drawer"> | |
| <div class="settings-header"> | |
| <span class="settings-title">Configuration Drawer</span> | |
| <button class="close-drawer-btn" onclick="toggleSettings()"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="18" y1="6" x2="6" y2="18"/> | |
| <line x1="6" y1="6" x2="18" y2="18"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="settings-group" id="hf-account-group" style="display:none;"> | |
| <label class="settings-label">Hugging Face Account</label> | |
| <div class="cli-info-banner" style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;"> | |
| <img id="hf-account-avatar" src="" alt="" style="width:28px; height:28px; border-radius:50%; flex-shrink:0;"> | |
| <span style="font-size:13px;"> | |
| Signed in as <strong id="hf-account-name"></strong> | |
| </span> | |
| <button onclick="hfLogout()" style="margin-left:auto; font-size:11px; padding:4px 10px; border-radius:6px; border:1px solid var(--sidebar-border); background:transparent; color:var(--text-color); cursor:pointer;">Sign out</button> | |
| </div> | |
| </div> | |
| <div class="settings-group"> | |
| <label class="settings-label" for="temperature-input">Temperature (Creativity)</label> | |
| <input type="range" id="temperature-input" min="0.1" max="1.5" step="0.1" value="0.7" oninput="document.getElementById('temp-val').innerText = this.value; saveConfig()"> | |
| <span id="temp-val" style="font-size:12px; margin-top:2px;">0.7</span> | |
| </div> | |
| <div class="settings-group"> | |
| <label class="settings-label" for="system-input">System Instruction / Agent Prompt</label> | |
| <textarea id="system-input" class="settings-input" rows="4" placeholder="You are a helpful assistant..." onchange="saveConfig()"></textarea> | |
| </div> | |
| <div class="cli-info-banner"> | |
| <strong>Terminal Mode Info:</strong><br> | |
| You can type commands directly in the chat bar!<br> | |
| • <code>/clear</code>: Reset chat<br> | |
| • <code>/theme</code>: Cycle styles<br> | |
| • <code>/help</code>: Display CLI manual<br> | |
| • <code>/matrix</code>: Toggle digital rain | |
| </div> | |
| </div> | |
| <!-- Main Pane --> | |
| <div class="workspace"> | |
| <!-- SVG Watermark Background 5.2 (Double Stencil Outline) --> | |
| <div class="watermark-container"> | |
| <svg class="watermark-svg" viewBox="0 0 100 100"> | |
| <path d="M 30,25 H 70 V 48 H 30 V 75 H 70" /> | |
| <path d="M 35,30 H 65 V 43 H 35 V 70 H 65" stroke-dasharray="2, 2" /> | |
| </svg> | |
| </div> | |
| <!-- Welcome Initial Panel --> | |
| <div class="welcome-panel" id="welcome-panel"> | |
| <h1 class="welcome-title">GLM 5.2</h1> | |
| <p class="welcome-subtitle" style="font-family: var(--font-serif); font-size: 24px; margin-bottom: 12px;">Create anything you can imagine</p> | |
| <!-- Sleek terminal status dashboard inside welcome screen --> | |
| <div class="welcome-terminal-box"> | |
| <div class="welcome-terminal-header"> | |
| <span class="terminal-dot red"></span> | |
| <span class="terminal-dot yellow"></span> | |
| <span class="terminal-dot green"></span> | |
| <span class="terminal-title">visitor@glm5.2: ~</span> | |
| </div> | |
| <pre class="welcome-terminal-content"><code>system_boot_sequence: SUCCESS | |
| model_identity : zai-org/GLM-5.2:fireworks-ai | |
| connection_state : ONLINE | |
| stream_completions : ACTIVE | |
| Type a query or /help to interact with GLM 5.2...</code></pre> | |
| </div> | |
| <p class="welcome-subtext" style="font-size: 13px; color: var(--text-muted); margin-top: 8px;">Interact with GLM 5.2 and explore the boundless creative world</p> | |
| </div> | |
| <!-- Chat Status Bar --> | |
| <div class="chat-status-bar" id="chat-status-bar"> | |
| <span class="status-item"><span class="status-dot"></span> GLM 5.2 CLI Session</span> | |
| <span class="status-item font-mono" style="font-family: var(--font-mono)">Model: zai-org/GLM-5.2:fireworks-ai</span> | |
| <span class="status-item" id="status-agent-mode">Mode: Standard</span> | |
| </div> | |
| <!-- Chat Logs Console --> | |
| <div class="chat-container" id="chat-container"></div> | |
| <!-- Input Bar (Matches Floating GLM-5.2 Layout) --> | |
| <div class="input-panel"> | |
| <div class="input-container"> | |
| <div class="pending-upload-row" id="pending-upload-row"> | |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg> | |
| <span id="pending-upload-name"></span> | |
| <span class="remove-upload" id="remove-upload-btn" onclick="clearPendingUpload()">×</span> | |
| </div> | |
| <div class="textarea-wrapper"> | |
| <textarea | |
| id="chat-input" | |
| class="chat-input" | |
| placeholder="Send a Message" | |
| rows="1" | |
| oninput="adjustInputHeight(this); checkInputLength(this)" | |
| onkeydown="handleKeyPress(event)" | |
| ></textarea> | |
| </div> | |
| <div class="input-controls"> | |
| <div class="controls-left"> | |
| <!-- Agent pill button (Standard / Terminal toggle) --> | |
| <div class="agent-capsule" id="agent-capsule" onclick="toggleAgentMode()"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="4 17 10 11 4 5"/> | |
| <line x1="12" y1="19" x2="20" y2="19"/> | |
| </svg> | |
| <span id="agent-pill-label">Standard Mode</span> | |
| </div> | |
| <!-- Attach file button --> | |
| <button class="attach-btn" id="attach-btn" onclick="document.getElementById('file-upload-input').click()" title="Attach a file to the workspace"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg> | |
| </button> | |
| <input type="file" id="file-upload-input" hidden onchange="handleFileUpload(event)"> | |
| </div> | |
| <!-- Send Up-Arrow Button --> | |
| <button class="send-btn" id="send-btn" onclick="submitChat()" disabled> | |
| <!-- Rounded arrow up --> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="12" y1="19" x2="12" y2="5"/> | |
| <polyline points="5 12 12 5 19 12"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Scripting and logic --> | |
| <script> | |
| // State variables | |
| let activeAgentMode = 'standard'; // standard, terminal | |
| let currentTheme = 'light'; // light, dark, classic-terminal | |
| let matrixInterval = null; // Digital rain interval reference | |
| let hfUser = null; // Logged-in Hugging Face user profile (null = signed out) | |
| let pendingUpload = null; // {name, size} staged via the paperclip button, attached to the next message | |
| let historyHydrated = false; // guards against re-hydrating the transcript more than once per session | |
| let hostTokenAvailable = false; // true if the Space owner set an HF_TOKEN secret (chat works without OAuth login) | |
| let activeProvider = 'hf'; // which GLM-5.2 backend this Space is configured to use: hf, nvidia, or zai | |
| // On Page Load: Hydrate settings from local storage | |
| window.addEventListener('DOMContentLoaded', () => { | |
| const savedTemp = localStorage.getItem('temp'); | |
| if (savedTemp) { | |
| document.getElementById('temperature-input').value = savedTemp; | |
| document.getElementById('temp-val').innerText = savedTemp; | |
| } | |
| const savedSystem = localStorage.getItem('system_prompt'); | |
| if (savedSystem) { | |
| document.getElementById('system-input').value = savedSystem; | |
| } | |
| const savedTheme = localStorage.getItem('theme'); | |
| if (savedTheme) { | |
| applyTheme(savedTheme); | |
| } | |
| // Hydrate Hugging Face auth state from the backend session. | |
| hydrateHfAuth(); | |
| }); | |
| // Refresh auth state instantly when the sign-in popup signals completion. | |
| window.addEventListener('message', (event) => { | |
| if (event && event.data && event.data.type === 'hf-auth-complete') { | |
| hydrateHfAuth(); | |
| } | |
| }); | |
| // Fetch the logged-in user (if any) from the backend session and update the UI. | |
| async function hydrateHfAuth() { | |
| try { | |
| const res = await fetch('/me'); | |
| const data = await res.json(); | |
| hostTokenAvailable = !!(data && data.host_available); | |
| activeProvider = (data && data.provider) || 'hf'; | |
| const authBtn = document.getElementById('hf-auth-btn'); | |
| if (authBtn) { | |
| // The HF sign-in button only means anything on the hf | |
| // provider -- other providers run on a single shared | |
| // owner secret with no per-visitor login. | |
| authBtn.style.display = (activeProvider === 'hf') ? '' : 'none'; | |
| } | |
| if (data && data.user) { | |
| setHfUser(data.user); | |
| } else { | |
| setHfUser(null); | |
| } | |
| } catch (err) { | |
| console.error('Failed to hydrate HF auth:', err); | |
| setHfUser(null); | |
| } | |
| } | |
| // Reflect the current HF auth state across the UI. | |
| function setHfUser(user) { | |
| hfUser = user; | |
| const authBtn = document.getElementById('hf-auth-btn'); | |
| const accountGroup = document.getElementById('hf-account-group'); | |
| if (user) { | |
| authBtn.classList.add('active'); | |
| authBtn.setAttribute('data-tooltip', `Signed in as ${user.preferred_username || user.name || 'HF user'} — click to sign out`); | |
| if (accountGroup) { | |
| accountGroup.style.display = 'block'; | |
| const name = document.getElementById('hf-account-name'); | |
| const avatar = document.getElementById('hf-account-avatar'); | |
| if (name) name.textContent = `@${user.preferred_username || user.name}`; | |
| if (avatar && user.picture) { | |
| avatar.src = user.picture; | |
| avatar.style.display = 'inline-block'; | |
| } else if (avatar) { | |
| avatar.style.display = 'none'; | |
| } | |
| } | |
| // Restore this person's saved conversation + workspace from the | |
| // server the first time we see them signed in this session. | |
| if (!historyHydrated) { | |
| historyHydrated = true; | |
| hydrateHistory(); | |
| } | |
| } else { | |
| authBtn.classList.remove('active'); | |
| authBtn.setAttribute('data-tooltip', 'Sign in with Hugging Face'); | |
| if (accountGroup) accountGroup.style.display = 'none'; | |
| // No OAuth session, but the Space owner may still have set a | |
| // host HF_TOKEN secret -- chat (and its saved history) works | |
| // in that case too, without anyone needing to click sign-in. | |
| if (hostTokenAvailable && !historyHydrated) { | |
| historyHydrated = true; | |
| hydrateHistory(); | |
| } else if (!hostTokenAvailable) { | |
| historyHydrated = false; | |
| } | |
| } | |
| } | |
| // Sidebar button: sign in (redirect) or, if already signed in, sign out. | |
| function handleHfAuthClick() { | |
| if (hfUser) { | |
| hfLogout(); | |
| } else { | |
| // Open the HF authorize page in a new tab to avoid iframe cookie | |
| // issues. Gradio's callback redirects the popup back to "/" (our | |
| // app). We hold the popup handle and poll /me; the moment the user | |
| // is signed in we refresh this tab's state and close the popup | |
| // ourselves (window.close() from the opener works across the | |
| // cross-origin OAuth hops, unlike window.close() inside the popup). | |
| const popup = window.open('/login/huggingface', '_blank'); | |
| let checksLeft = 120; // ~60s | |
| const checkAuth = setInterval(async () => { | |
| checksLeft -= 1; | |
| // If the popup was closed early (e.g. user cancelled), stop polling. | |
| if (popup && popup.closed) { | |
| clearInterval(checkAuth); | |
| hydrateHfAuth(); | |
| return; | |
| } | |
| try { | |
| const res = await fetch('/me'); | |
| const data = await res.json(); | |
| if (data && data.user) { | |
| setHfUser(data.user); | |
| try { popup.close(); } catch (e) {} | |
| clearInterval(checkAuth); | |
| return; | |
| } | |
| } catch (e) { /* ignore transient fetch errors */ } | |
| if (checksLeft <= 0) clearInterval(checkAuth); | |
| }, 500); | |
| } | |
| } | |
| // Sign out of HF and refresh auth state. | |
| async function hfLogout() { | |
| try { | |
| await fetch('/logout'); | |
| } catch (err) { | |
| console.error('Logout request failed:', err); | |
| } | |
| setHfUser(null); | |
| } | |
| // Config Drawer toggles | |
| function toggleSettings() { | |
| const drawer = document.getElementById('settings-drawer'); | |
| drawer.classList.toggle('open'); | |
| } | |
| function saveConfig() { | |
| const temp = document.getElementById('temperature-input').value; | |
| const system = document.getElementById('system-input').value.trim(); | |
| localStorage.setItem('temp', temp); | |
| localStorage.setItem('system_prompt', system); | |
| } | |
| // Navigation back home | |
| function goToHome() { | |
| document.getElementById('welcome-panel').style.display = 'flex'; | |
| document.getElementById('chat-container').style.display = 'none'; | |
| document.getElementById('chat-status-bar').style.display = 'none'; | |
| } | |
| // Clear Workspace History (both the visible transcript and the | |
| // server-side memory it's persisted to -- files already created in | |
| // the workspace are left alone, this only forgets the conversation). | |
| function clearSession() { | |
| const container = document.getElementById('chat-container'); | |
| container.innerHTML = ''; | |
| clearPendingUpload(); | |
| if (hfUser) { | |
| fetch('/api/history/clear', { method: 'POST' }).catch((err) => { | |
| console.error('Failed to clear server-side history:', err); | |
| }); | |
| } | |
| // Log local terminal reset | |
| appendLocalLog("Session reset. Terminal memory buffer cleared."); | |
| goToHome(); | |
| } | |
| // Append locally printed logs (for CLI actions) | |
| function appendLocalLog(text) { | |
| const container = document.getElementById('chat-container'); | |
| // Check if welcome panel is visible, hide it | |
| if (document.getElementById('welcome-panel').style.display !== 'none') { | |
| document.getElementById('welcome-panel').style.display = 'none'; | |
| container.style.display = 'flex'; | |
| document.getElementById('chat-status-bar').style.display = 'flex'; | |
| } | |
| const logRow = document.createElement('div'); | |
| logRow.className = 'message-row'; | |
| logRow.innerHTML = ` | |
| <div class="prompt-header"> | |
| <span class="prompt-prefix-system">system@glm5.2:~$</span> | |
| <span class="prompt-command">info</span> | |
| </div> | |
| <div class="prompt-text system-log">${text}</div> | |
| `; | |
| container.appendChild(logRow); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| // Adjusts input textarea height depending on characters typed | |
| function adjustInputHeight(textarea) { | |
| textarea.style.height = 'auto'; | |
| textarea.style.height = (textarea.scrollHeight) + 'px'; | |
| } | |
| // Toggles the send button color and activity state based on input presence | |
| function checkInputLength(textarea) { | |
| const sendBtn = document.getElementById('send-btn'); | |
| if (textarea.value.trim().length > 0) { | |
| sendBtn.classList.add('active'); | |
| sendBtn.disabled = false; | |
| } else { | |
| sendBtn.classList.remove('active'); | |
| sendBtn.disabled = true; | |
| } | |
| } | |
| // Key Press handling: Shift+Enter inserts newline, Enter submits | |
| function handleKeyPress(event) { | |
| if (event.key === 'Enter' && !event.shiftKey) { | |
| event.preventDefault(); | |
| submitChat(); | |
| } | |
| } | |
| // Toggles theme through cycle: Light -> Dark -> Retro CLI Terminal | |
| function cycleTheme() { | |
| if (currentTheme === 'light') { | |
| applyTheme('dark'); | |
| } else if (currentTheme === 'dark') { | |
| applyTheme('classic-terminal'); | |
| } else { | |
| applyTheme('light'); | |
| } | |
| } | |
| function applyTheme(themeName) { | |
| document.body.classList.remove('dark-mode', 'classic-terminal-mode'); | |
| const matrixCanvas = document.getElementById('matrix-canvas'); | |
| // Stop matrix loop unless in classic-terminal-mode | |
| if (matrixInterval) { | |
| clearInterval(matrixInterval); | |
| matrixInterval = null; | |
| } | |
| matrixCanvas.style.display = 'none'; | |
| if (themeName === 'dark') { | |
| document.body.classList.add('dark-mode'); | |
| currentTheme = 'dark'; | |
| } else if (themeName === 'classic-terminal') { | |
| document.body.classList.add('classic-terminal-mode'); | |
| currentTheme = 'classic-terminal'; | |
| initMatrixRain(); | |
| } else { | |
| currentTheme = 'light'; | |
| } | |
| localStorage.setItem('theme', currentTheme); | |
| } | |
| // Toggle Agents (Terminal mode or Standard chat mode) | |
| function toggleAgentMode() { | |
| const pillLabel = document.getElementById('agent-pill-label'); | |
| const systemTextarea = document.getElementById('system-input'); | |
| const chatContainer = document.getElementById('chat-container'); | |
| if (activeAgentMode === 'standard') { | |
| activeAgentMode = 'terminal'; | |
| pillLabel.innerText = "Terminal Mode"; | |
| systemTextarea.value = "You are a senior system administrator and Unix terminal helper. Answer requests using command-line commands, scripts, code configurations, and wrap instructions in monospaced outputs."; | |
| appendLocalLog("Agent profile set to: Unix Terminal CLI assistant."); | |
| chatContainer.classList.add('terminal-layout'); | |
| } else { | |
| activeAgentMode = 'standard'; | |
| pillLabel.innerText = "Standard Mode"; | |
| systemTextarea.value = ""; | |
| appendLocalLog("Agent profile restored to: default general assistant."); | |
| chatContainer.classList.remove('terminal-layout'); | |
| } | |
| const statusAgentLabel = document.getElementById('status-agent-mode'); | |
| if (statusAgentLabel) { | |
| statusAgentLabel.innerText = "Mode: " + (activeAgentMode === 'standard' ? 'Standard' : 'Terminal'); | |
| } | |
| const statusBar = document.getElementById('chat-status-bar'); | |
| if (statusBar) { | |
| statusBar.style.display = (activeAgentMode === 'terminal' && chatContainer.style.display === 'flex') ? 'flex' : 'none'; | |
| } | |
| saveConfig(); | |
| } | |
| // Handle slash command local executes | |
| async function handleSlashCommand(commandStr) { | |
| const container = document.getElementById('chat-container'); | |
| const cmd = commandStr.trim(); | |
| // Append user prompt action to screen | |
| const promptRow = document.createElement('div'); | |
| promptRow.className = 'message-row'; | |
| promptRow.innerHTML = ` | |
| <div class="prompt-header"> | |
| <span class="prompt-prefix-user">visitor@glm5.2:~$</span> | |
| <span class="prompt-command">run</span> | |
| </div> | |
| <div class="prompt-text">${cmd}</div> | |
| `; | |
| container.appendChild(promptRow); | |
| if (cmd === '/clear') { | |
| clearSession(); | |
| return; | |
| } | |
| // Create response layout for command output | |
| const respRow = document.createElement('div'); | |
| respRow.className = 'message-row'; | |
| let outputText = ''; | |
| if (cmd.startsWith('/help')) { | |
| outputText = ` | |
| <strong>GLM-5.2 CLI SYSTEM MANUAL</strong> | |
| ---------------------------------------- | |
| Here is a list of available command line functions: | |
| • <code>/help</code> - Prints this helper instruction table. | |
| • <code>/clear</code> - Resets active session logs and moves to welcome home panel. | |
| • <code>/theme</code> - Alternates theme context (Light -> Dark -> Classic green terminal). | |
| • <code>/matrix</code> - Toggles background matrix digital rain canvas. | |
| • <code>/system [prompt]</code> - Updates assistant's instruction context in config dynamically. | |
| • <code>/files</code> - Lists files currently in your private workspace, with download links. | |
| ---------------------------------------- | |
| `; | |
| } else if (cmd.startsWith('/files')) { | |
| if (!hfUser && !hostTokenAvailable) { | |
| outputText = notReadyMessage(); | |
| } else { | |
| try { | |
| const res = await fetch('/api/workspace'); | |
| const data = await res.json(); | |
| const files = (data && data.files) || []; | |
| if (files.length === 0) { | |
| outputText = 'Your workspace is empty right now. Ask the assistant to build something!'; | |
| } else { | |
| const lines = files.map(f => | |
| `• <code>${escapeHtml(f.name)}</code> — ${formatBytes(f.size)} — <a href="/api/files/${encodeURIComponent(f.name)}" download>download</a>` | |
| ); | |
| const persistNote = data.persistent | |
| ? '' | |
| : '<br><em>Note: no persistent storage is attached to this Space, so these files reset if it restarts.</em>'; | |
| outputText = `<strong>Workspace files</strong><br>${lines.join('<br>')}${persistNote}`; | |
| } | |
| } catch (err) { | |
| outputText = `Failed to list workspace files: ${escapeHtml(err.message)}`; | |
| } | |
| } | |
| } else if (cmd.startsWith('/theme')) { | |
| cycleTheme(); | |
| outputText = `Theme toggled to: <strong>${currentTheme.toUpperCase()}</strong>`; | |
| } else if (cmd.startsWith('/matrix')) { | |
| const canvas = document.getElementById('matrix-canvas'); | |
| if (canvas.style.display === 'block') { | |
| canvas.style.display = 'none'; | |
| if (matrixInterval) clearInterval(matrixInterval); | |
| matrixInterval = null; | |
| outputText = "Digital matrix rain canvas disabled."; | |
| } else { | |
| canvas.style.display = 'block'; | |
| initMatrixRain(); | |
| outputText = "Digital matrix rain canvas enabled."; | |
| } | |
| } else if (cmd.startsWith('/system ')) { | |
| const sysText = cmd.substring(8); | |
| document.getElementById('system-input').value = sysText; | |
| saveConfig(); | |
| outputText = `System prompt set to: <em>"${sysText}"</em>`; | |
| } else { | |
| outputText = `Unknown system command: <code>${cmd}</code>. Type <code>/help</code> for list of available CLI commands.`; | |
| } | |
| respRow.innerHTML = ` | |
| <div class="prompt-header"> | |
| <span class="prompt-prefix-system">system@glm5.2:~$</span> | |
| <span class="prompt-command">stdout</span> | |
| </div> | |
| <div class="message-content">${outputText}</div> | |
| `; | |
| container.appendChild(respRow); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| // Submits user message to the API endpoint and streams the output | |
| async function submitChat() { | |
| const inputField = document.getElementById('chat-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const welcomePanel = document.getElementById('welcome-panel'); | |
| const chatContainer = document.getElementById('chat-container'); | |
| const text = inputField.value.trim(); | |
| if (!text) return; | |
| // Gate inference behind either an HF OAuth session or a host | |
| // HF_TOKEN secret the Space owner configured -- requests are | |
| // billed to whichever of those pays for the call. | |
| if (!hfUser && !hostTokenAvailable) { | |
| inputField.value = ''; | |
| inputField.style.height = 'auto'; | |
| sendBtn.classList.remove('active'); | |
| sendBtn.disabled = true; | |
| const needLoginRow = document.createElement('div'); | |
| needLoginRow.className = `message-row assistant-row style-${activeAgentMode}`; | |
| needLoginRow.innerHTML = ` | |
| <div class="prompt-header"> | |
| <span class="prompt-prefix-system">system@glm5.2:~$</span> | |
| <span class="prompt-command">auth_required</span> | |
| </div> | |
| <div class="message-content">${notReadyMessage()}</div> | |
| `; | |
| chatContainer.appendChild(needLoginRow); | |
| if (welcomePanel.style.display !== 'none') { | |
| welcomePanel.style.display = 'none'; | |
| chatContainer.style.display = 'flex'; | |
| } | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| return; | |
| } | |
| // Reset Input field | |
| inputField.value = ''; | |
| inputField.style.height = 'auto'; | |
| sendBtn.classList.remove('active'); | |
| sendBtn.disabled = true; | |
| // Check if user is typing a local command | |
| if (text.startsWith('/')) { | |
| // Check if welcome page is visible | |
| if (welcomePanel.style.display !== 'none') { | |
| welcomePanel.style.display = 'none'; | |
| chatContainer.style.display = 'flex'; | |
| if (activeAgentMode === 'terminal') { | |
| document.getElementById('chat-status-bar').style.display = 'flex'; | |
| } | |
| } | |
| await handleSlashCommand(text); | |
| return; | |
| } | |
| // Clean UI welcome state | |
| if (welcomePanel.style.display !== 'none') { | |
| welcomePanel.style.display = 'none'; | |
| chatContainer.style.display = 'flex'; | |
| if (activeAgentMode === 'terminal') { | |
| document.getElementById('chat-status-bar').style.display = 'flex'; | |
| } | |
| } | |
| // Fold any pending upload (paperclip attachment) into what gets | |
| // sent, so the agent knows the file is already in its workspace. | |
| const upload = pendingUpload; | |
| clearPendingUpload(); | |
| let messageToSend = text; | |
| if (upload) { | |
| messageToSend = `[I attached a file to my workspace: "${upload.name}" (${formatBytes(upload.size)}), available at that relative path.]\n\n${text}`; | |
| } | |
| // Append User message row | |
| const userRow = document.createElement('div'); | |
| userRow.className = `message-row user-row style-${activeAgentMode}`; | |
| const userHeaderHtml = activeAgentMode === 'standard' | |
| ? `<div class="prompt-header"><span class="standard-sender-user">You</span></div>` | |
| : `<div class="prompt-header"><span class="prompt-prefix-user">visitor@glm5.2:~$</span><span class="prompt-command">prompt</span></div>`; | |
| const uploadChipHtml = upload | |
| ? `<div class="file-chip" style="margin-top:8px; cursor: default;"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg> | |
| <span>${escapeHtml(upload.name)}</span> | |
| <span class="file-chip-meta">${formatBytes(upload.size)}</span> | |
| </div>` | |
| : ''; | |
| userRow.innerHTML = `${userHeaderHtml}<div class="prompt-text">${escapeHtml(text)}</div>${uploadChipHtml}`; | |
| chatContainer.appendChild(userRow); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| const temperatureVal = parseFloat(document.getElementById('temperature-input').value); | |
| const systemPromptVal = document.getElementById('system-input').value.trim(); | |
| // --- Per-turn rendering helpers ----------------------------------- | |
| // The agent loop can run several model turns (reasoning -> tool | |
| // call -> tool result -> reasoning -> ... -> final answer) for one | |
| // user message. Each turn gets its own bubble; tool calls render | |
| // as distinct cards in between, exactly as they happen. | |
| let currentBubble = null; // { row, contentEl, raw } | |
| function startNewBubble() { | |
| const streamId = 'stream-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7); | |
| const row = document.createElement('div'); | |
| row.className = `message-row assistant-row style-${activeAgentMode}`; | |
| if (activeAgentMode === 'standard') { | |
| row.innerHTML = ` | |
| <div class="prompt-header"><span class="standard-sender-system">GLM 5.2</span></div> | |
| <div class="message-content" id="${streamId}"><span class="standard-loader">typing...</span></div> | |
| `; | |
| } else { | |
| row.innerHTML = ` | |
| <div class="prompt-header"><span class="prompt-prefix-system">system@glm5.2:~$</span><span class="prompt-command">./run_glm_5.2_agent</span></div> | |
| <div class="message-content" id="${streamId}"><span class="terminal-cursor"></span></div> | |
| `; | |
| } | |
| chatContainer.appendChild(row); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| currentBubble = { row, contentEl: document.getElementById(streamId), raw: '' }; | |
| } | |
| function appendToken(delta) { | |
| if (!currentBubble) startNewBubble(); | |
| currentBubble.raw += delta; | |
| const cursor = activeAgentMode === 'terminal' ? '<span class="terminal-cursor"></span>' : ''; | |
| currentBubble.contentEl.innerHTML = marked.parse(currentBubble.raw) + cursor; | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| // Finalizes the open bubble. If cleanText is given, that replaces | |
| // the raw streamed text (used to strip a tool_call JSON block back | |
| // out once we know it was there); an empty result removes the | |
| // bubble entirely rather than showing a blank reply. | |
| function finalizeBubble(cleanText) { | |
| if (!currentBubble) return; | |
| const text = (cleanText !== undefined && cleanText !== null) ? cleanText : currentBubble.raw; | |
| if (!text || !text.trim()) { | |
| currentBubble.row.remove(); | |
| } else { | |
| currentBubble.contentEl.innerHTML = marked.parse(text); | |
| } | |
| currentBubble = null; | |
| } | |
| function renderToolCall(tool, args) { | |
| const card = document.createElement('div'); | |
| card.className = 'tool-card'; | |
| let argsPreview = ''; | |
| try { argsPreview = JSON.stringify(args || {}); } catch (e) { /* ignore */ } | |
| card.innerHTML = ` | |
| <div class="tool-card-header"> | |
| <span class="tool-name">${escapeHtml(tool || 'tool')}</span> | |
| <span class="tool-args-preview">${escapeHtml(argsPreview)}</span> | |
| <span class="tool-status pending">running</span> | |
| </div> | |
| <div class="tool-card-body" style="display:none;"></div> | |
| `; | |
| chatContainer.appendChild(card); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| return card; | |
| } | |
| function fillToolResult(card, payload) { | |
| if (!card) return; | |
| const statusEl = card.querySelector('.tool-status'); | |
| const bodyEl = card.querySelector('.tool-card-body'); | |
| statusEl.textContent = payload.success ? 'done' : 'failed'; | |
| statusEl.classList.remove('pending'); | |
| statusEl.classList.add(payload.success ? 'success' : 'fail'); | |
| let bodyHtml = ''; | |
| if (payload.stdout) bodyHtml += `<pre class="tool-card-output">${escapeHtml(payload.stdout)}</pre>`; | |
| if (payload.stderr) bodyHtml += `<pre class="tool-card-output stderr-block">${escapeHtml(payload.stderr)}</pre>`; | |
| if (!payload.stdout && !payload.stderr) bodyHtml += `<div class="tool-card-file-line">(no output)</div>`; | |
| bodyEl.innerHTML = bodyHtml; | |
| bodyEl.style.display = 'block'; | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| function renderFileChip(name, url, size) { | |
| const a = document.createElement('a'); | |
| a.className = 'file-chip'; | |
| a.href = url; | |
| a.setAttribute('download', name); | |
| a.innerHTML = ` | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | |
| <span>${escapeHtml(name)}</span> | |
| <span class="file-chip-meta">${size != null ? formatBytes(size) + ' — ' : ''}Download</span> | |
| `; | |
| chatContainer.appendChild(a); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } | |
| let pendingToolCard = null; | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| message: messageToSend, | |
| temperature: temperatureVal, | |
| system_prompt: systemPromptVal || null, | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Connection failure: Status ${response.status}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| while (true) { | |
| const { value, done } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| // Retain trailing partial line in buffer | |
| buffer = lines.pop(); | |
| for (const line of lines) { | |
| const cleanLine = line.trim(); | |
| if (!cleanLine || !cleanLine.startsWith('data: ')) continue; | |
| const dataStr = cleanLine.substring(6); | |
| let payload; | |
| try { | |
| payload = JSON.parse(dataStr); | |
| } catch (err) { | |
| console.error('Failed to unpack payload chunk:', cleanLine, err); | |
| continue; | |
| } | |
| switch (payload.type) { | |
| case 'turn_start': | |
| startNewBubble(); | |
| break; | |
| case 'token': | |
| appendToken(payload.content || ''); | |
| break; | |
| case 'tool_call': | |
| finalizeBubble(payload.text); | |
| pendingToolCard = renderToolCall(payload.tool, payload.args); | |
| break; | |
| case 'tool_result': | |
| if (payload.tool === null) { | |
| // Malformed tool_call JSON: no tool_call event preceded this, | |
| // so finalize the reasoning bubble and show the parse error inline. | |
| finalizeBubble(payload.text); | |
| const errCard = renderToolCall('tool_call (format error)', {}); | |
| fillToolResult(errCard, payload); | |
| } else if (pendingToolCard) { | |
| fillToolResult(pendingToolCard, payload); | |
| pendingToolCard = null; | |
| } | |
| break; | |
| case 'file': | |
| renderFileChip(payload.name, payload.url, payload.size); | |
| break; | |
| case 'error': | |
| if (!currentBubble) startNewBubble(); | |
| finalizeBubble(`**Error:** ${payload.message}`); | |
| break; | |
| case 'done': | |
| finalizeBubble(); // no-op if already finalized; else renders the final answer as-is | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| finalizeBubble(); // safety net in case the stream ended without an explicit 'done' | |
| } catch (err) { | |
| if (!currentBubble) startNewBubble(); | |
| finalizeBubble(`**Connection error:** ${err.message}`); | |
| console.error(err); | |
| } | |
| } | |
| // Helper to prevent tag insertions in text content | |
| function escapeHtml(text) { | |
| return text | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| // Human-readable file sizes for chips and /files output | |
| function formatBytes(n) { | |
| if (n === null || n === undefined) return ''; | |
| if (n < 1024) return n + ' B'; | |
| if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'; | |
| return (n / (1024 * 1024)).toFixed(2) + ' MB'; | |
| } | |
| // What to tell the person when chat isn't usable yet -- wording | |
| // depends on which backend this Space is configured to use. | |
| function providerEnvVarName() { | |
| if (activeProvider === 'nvidia') return 'NVIDIA_API_KEY'; | |
| if (activeProvider === 'zai') return 'ZAI_API_KEY'; | |
| if (activeProvider === 'zhipu') return 'ZHIPU_API_KEY'; | |
| return null; | |
| } | |
| function notReadyMessage() { | |
| if (activeProvider === 'hf') { | |
| return 'Sign in with Hugging Face to run inference billed to your own account. Click the <strong>HF</strong> button in the left sidebar to sign in.'; | |
| } | |
| const envVar = providerEnvVarName() || 'the provider API key'; | |
| return `This Space isn't configured yet. Ask the owner to add a <strong>${envVar}</strong> secret in the Space settings (Settings → Variables and secrets).`; | |
| } | |
| // Paperclip button: stage an uploaded file into the private workspace | |
| // ahead of the next message, so the agent can read/process it. | |
| async function handleFileUpload(event) { | |
| const input = event.target; | |
| const file = input.files && input.files[0]; | |
| input.value = ''; // allow re-selecting the same file later | |
| if (!file) return; | |
| if (!hfUser && !hostTokenAvailable) { | |
| const envVar = providerEnvVarName(); | |
| alert(envVar | |
| ? `This Space isn't configured yet. Ask the owner to add a ${envVar} secret in the Space settings.` | |
| : 'Sign in with Hugging Face first to attach files to your workspace.'); | |
| return; | |
| } | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const res = await fetch('/api/upload', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| if (!res.ok || data.error) { | |
| throw new Error(data.error || `Upload failed (status ${res.status})`); | |
| } | |
| pendingUpload = { name: data.name, size: data.size }; | |
| const row = document.getElementById('pending-upload-row'); | |
| document.getElementById('pending-upload-name').textContent = `${data.name} (${formatBytes(data.size)}) attached — will be sent with your next message`; | |
| row.classList.add('visible'); | |
| } catch (err) { | |
| console.error('Upload failed:', err); | |
| alert('Upload failed: ' + err.message); | |
| } | |
| } | |
| function clearPendingUpload() { | |
| pendingUpload = null; | |
| const row = document.getElementById('pending-upload-row'); | |
| if (row) row.classList.remove('visible'); | |
| const nameEl = document.getElementById('pending-upload-name'); | |
| if (nameEl) nameEl.textContent = ''; | |
| } | |
| // Restore a signed-in user's saved conversation (and the tool calls, | |
| // results, and files that happened in it) from the server on load. | |
| async function hydrateHistory() { | |
| try { | |
| const res = await fetch('/api/history'); | |
| const data = await res.json(); | |
| const items = (data && data.history) || []; | |
| const chatContainer = document.getElementById('chat-container'); | |
| const welcomePanel = document.getElementById('welcome-panel'); | |
| chatContainer.innerHTML = ''; | |
| if (items.length === 0) return; // nothing saved yet, stay on the welcome panel | |
| welcomePanel.style.display = 'none'; | |
| chatContainer.style.display = 'flex'; | |
| if (activeAgentMode === 'terminal') { | |
| document.getElementById('chat-status-bar').style.display = 'flex'; | |
| } | |
| let lastToolArgs = {}; | |
| for (const item of items) { | |
| try { | |
| if (item.type === 'assistant_text') { | |
| renderHistoryTextRow(chatContainer, 'assistant', item.text || ''); | |
| lastToolArgs = (item.tool_call && item.tool_call.args) || {}; | |
| } else if (item.type === 'user') { | |
| renderHistoryTextRow(chatContainer, 'user', item.text || ''); | |
| } else if (item.type === 'tool_result') { | |
| renderHistoryToolResult(chatContainer, item, lastToolArgs); | |
| lastToolArgs = {}; | |
| } | |
| } catch (itemErr) { | |
| console.error('Failed to render a history item, skipping it:', item, itemErr); | |
| } | |
| } | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| } catch (err) { | |
| console.error('Failed to hydrate chat history:', err); | |
| } | |
| } | |
| function renderHistoryTextRow(container, who, text) { | |
| const clean = (text || '').trim(); | |
| if (who === 'assistant' && !clean) return; // tool-call-only turn, nothing visible to replay | |
| const row = document.createElement('div'); | |
| if (who === 'user') { | |
| row.className = `message-row user-row style-${activeAgentMode}`; | |
| const header = activeAgentMode === 'standard' | |
| ? `<div class="prompt-header"><span class="standard-sender-user">You</span></div>` | |
| : `<div class="prompt-header"><span class="prompt-prefix-user">visitor@glm5.2:~$</span><span class="prompt-command">prompt</span></div>`; | |
| row.innerHTML = `${header}<div class="prompt-text">${escapeHtml(clean)}</div>`; | |
| } else { | |
| row.className = `message-row assistant-row style-${activeAgentMode}`; | |
| const header = activeAgentMode === 'standard' | |
| ? `<div class="prompt-header"><span class="standard-sender-system">GLM 5.2</span></div>` | |
| : `<div class="prompt-header"><span class="prompt-prefix-system">system@glm5.2:~$</span><span class="prompt-command">./run_glm_5.2_agent</span></div>`; | |
| row.innerHTML = `${header}<div class="message-content">${marked.parse(clean)}</div>`; | |
| } | |
| container.appendChild(row); | |
| } | |
| function renderHistoryToolResult(container, item, args) { | |
| const card = document.createElement('div'); | |
| card.className = 'tool-card'; | |
| let argsPreview = ''; | |
| try { argsPreview = JSON.stringify(args || {}); } catch (e) { /* ignore */ } | |
| card.innerHTML = ` | |
| <div class="tool-card-header"> | |
| <span class="tool-name">${escapeHtml(item.tool || 'tool_call (format error)')}</span> | |
| <span class="tool-args-preview">${escapeHtml(argsPreview)}</span> | |
| <span class="tool-status ${item.success ? 'success' : 'fail'}">${item.success ? 'done' : 'failed'}</span> | |
| </div> | |
| <div class="tool-card-body"></div> | |
| `; | |
| const bodyEl = card.querySelector('.tool-card-body'); | |
| let bodyHtml = ''; | |
| if (item.stdout) bodyHtml += `<pre class="tool-card-output">${escapeHtml(item.stdout)}</pre>`; | |
| if (item.stderr) bodyHtml += `<pre class="tool-card-output stderr-block">${escapeHtml(item.stderr)}</pre>`; | |
| if (!item.stdout && !item.stderr) bodyHtml += `<div class="tool-card-file-line">(no output)</div>`; | |
| bodyEl.innerHTML = bodyHtml; | |
| container.appendChild(card); | |
| if (item.file) { | |
| const a = document.createElement('a'); | |
| a.className = 'file-chip'; | |
| a.href = `/api/files/${encodeURIComponent(item.file)}`; | |
| a.setAttribute('download', item.file); | |
| a.innerHTML = ` | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> | |
| <span>${escapeHtml(item.file)}</span> | |
| <span class="file-chip-meta">Download</span> | |
| `; | |
| container.appendChild(a); | |
| } | |
| } | |
| // Digital Code Rain Matrix Animation | |
| function initMatrixRain() { | |
| const canvas = document.getElementById('matrix-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.style.display = 'block'; | |
| // Resize canvas to cover viewport | |
| function resizeCanvas() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Matrix character list | |
| const matrixChars = "01010101XYZGLM52SYSTEMCLIWORKSPACECONNECT"; | |
| const fontArr = matrixChars.split(""); | |
| const fontSize = 14; | |
| const columns = canvas.width / fontSize; | |
| // Track drop positions vertically | |
| const drops = []; | |
| for (let x = 0; x < columns; x++) { | |
| drops[x] = 1; | |
| } | |
| function drawMatrix() { | |
| // Set light/dark overlay blending depending on theme | |
| ctx.fillStyle = currentTheme === 'classic-terminal' ? 'rgba(5, 8, 6, 0.04)' : 'rgba(9, 9, 11, 0.04)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Set green text | |
| ctx.fillStyle = currentTheme === 'classic-terminal' ? '#33ff33' : '#a1a1aa'; | |
| ctx.font = fontSize + "px monospace"; | |
| for (let i = 0; i < drops.length; i++) { | |
| const text = fontArr[Math.floor(Math.random() * fontArr.length)]; | |
| ctx.fillText(text, i * fontSize, drops[i] * fontSize); | |
| // Reset drop when hitting screen bottom boundary | |
| if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) { | |
| drops[i] = 0; | |
| } | |
| drops[i]++; | |
| } | |
| } | |
| if (matrixInterval) clearInterval(matrixInterval); | |
| matrixInterval = setInterval(drawMatrix, 35); | |
| } | |
| </script> | |
| </body> | |
| </html> | |