Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>North Mini Code 1.0</title> | |
| <meta name="description" content="Terminal-style coding assistant powered by North Mini Code 1.0"> | |
| <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=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* ═══════════════════════════════════════════════════════ | |
| RESET & BASE | |
| ═══════════════════════════════════════════════════════ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg-deep: #0a0e14; | |
| --bg-panel: #0d1117; | |
| --bg-code: #161b22; | |
| --border: #1e2a3a; | |
| --border-focus: #2d4a6a; | |
| --green: #39ff14; | |
| --green-dim: #1a7a0a; | |
| --cyan: #00d4ff; | |
| --amber: #ffb300; | |
| --gray-light: #e0e0e0; | |
| --gray-mid: #8b949e; | |
| --gray-dim: #484f58; | |
| --red: #ff5555; | |
| --success: #50fa7b; | |
| --code-text: #f8f8f2; | |
| --glow-green: 0 0 8px rgba(57,255,20,0.3); | |
| --glow-cyan: 0 0 8px rgba(0,212,255,0.3); | |
| --glow-amber: 0 0 8px rgba(255,179,0,0.2); | |
| --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; | |
| --radius: 4px; | |
| --transition: 0.2s ease; | |
| } | |
| html, body { | |
| height: 100%; | |
| background: var(--bg-deep); | |
| color: var(--gray-light); | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| line-height: 1.6; | |
| overflow: hidden; | |
| } | |
| /* CRT scanline overlay */ | |
| body::after { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 9999; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(0, 0, 0, 0.03) 2px, | |
| rgba(0, 0, 0, 0.03) 4px | |
| ); | |
| } | |
| ::selection { | |
| background: rgba(57, 255, 20, 0.25); | |
| color: #fff; | |
| } | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--gray-dim); } | |
| a { color: var(--cyan); text-decoration: none; } | |
| a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); } | |
| /* ═══════════════════════════════════════════════════════ | |
| APP SHELL | |
| ═══════════════════════════════════════════════════════ */ | |
| #app { | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| max-height: 100vh; | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| CONFIG WARNING | |
| ═══════════════════════════════════════════════════════ */ | |
| #config-warning { | |
| display: none; | |
| background: linear-gradient(90deg, rgba(255,85,85,0.1), rgba(255,179,0,0.1)); | |
| border-bottom: 1px solid var(--red); | |
| padding: 8px 16px; | |
| font-size: 12px; | |
| color: var(--amber); | |
| text-align: center; | |
| text-shadow: var(--glow-amber); | |
| } | |
| #config-warning.visible { display: block; } | |
| /* ═══════════════════════════════════════════════════════ | |
| HEADER | |
| ═══════════════════════════════════════════════════════ */ | |
| #header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 12px 20px; | |
| background: var(--bg-panel); | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| gap: 16px; | |
| } | |
| .header-title { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| } | |
| .header-ascii { | |
| color: var(--green); | |
| font-size: 11px; | |
| line-height: 1.3; | |
| text-shadow: var(--glow-green); | |
| white-space: pre; | |
| letter-spacing: 0.5px; | |
| } | |
| .header-subtitle { | |
| color: var(--gray-mid); | |
| font-size: 11px; | |
| padding-left: 3px; | |
| } | |
| .header-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| flex-shrink: 0; | |
| } | |
| .pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 4px 10px; | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| font-size: 11px; | |
| color: var(--gray-mid); | |
| text-decoration: none; | |
| transition: all var(--transition); | |
| } | |
| .pill:hover { | |
| border-color: var(--cyan); | |
| color: var(--cyan); | |
| text-decoration: none; | |
| text-shadow: var(--glow-cyan); | |
| } | |
| .pill .dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: var(--success); | |
| box-shadow: 0 0 6px var(--success); | |
| } | |
| #btn-new-chat { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--amber); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| padding: 5px 12px; | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| letter-spacing: 1px; | |
| } | |
| #btn-new-chat:hover { | |
| border-color: var(--amber); | |
| background: rgba(255,179,0,0.08); | |
| text-shadow: var(--glow-amber); | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| MAIN LAYOUT | |
| ═══════════════════════════════════════════════════════ */ | |
| #main { | |
| display: flex; | |
| flex: 1; | |
| min-height: 0; | |
| overflow: hidden; | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| TERMINAL (LEFT PANEL) | |
| ═══════════════════════════════════════════════════════ */ | |
| #terminal-panel { | |
| display: flex; | |
| flex-direction: column; | |
| flex: 1; | |
| min-width: 0; | |
| border-right: 1px solid var(--border); | |
| } | |
| .panel-label { | |
| padding: 6px 16px; | |
| font-size: 10px; | |
| letter-spacing: 2px; | |
| color: var(--gray-dim); | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(13,17,23,0.6); | |
| text-transform: uppercase; | |
| flex-shrink: 0; | |
| } | |
| #chat-messages { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 16px; | |
| scroll-behavior: smooth; | |
| } | |
| /* Message styles */ | |
| .msg { | |
| margin-bottom: 14px; | |
| line-height: 1.65; | |
| animation: msgFadeIn 0.25s ease; | |
| } | |
| @keyframes msgFadeIn { | |
| from { opacity: 0; transform: translateY(6px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .msg-prefix { | |
| font-weight: 600; | |
| margin-right: 4px; | |
| } | |
| .msg-user .msg-prefix { | |
| color: var(--green); | |
| text-shadow: var(--glow-green); | |
| } | |
| .msg-user .msg-content { | |
| color: var(--green); | |
| text-shadow: var(--glow-green); | |
| } | |
| .msg-assistant .msg-prefix { | |
| color: var(--cyan); | |
| text-shadow: var(--glow-cyan); | |
| } | |
| .msg-assistant .msg-content { | |
| color: var(--gray-light); | |
| } | |
| .msg-system .msg-prefix { | |
| color: var(--amber); | |
| text-shadow: var(--glow-amber); | |
| } | |
| .msg-system .msg-content { | |
| color: var(--amber); | |
| opacity: 0.85; | |
| } | |
| /* Markdown elements */ | |
| .msg-content strong { color: #fff; font-weight: 600; } | |
| .msg-content em { font-style: italic; color: var(--gray-mid); } | |
| .msg-content code:not(pre code) { | |
| background: var(--bg-code); | |
| color: var(--code-text); | |
| padding: 1px 5px; | |
| border-radius: 3px; | |
| font-size: 12px; | |
| border: 1px solid var(--border); | |
| } | |
| .msg-content a { color: var(--cyan); } | |
| .msg-content ul, .msg-content ol { | |
| margin: 6px 0 6px 20px; | |
| } | |
| .msg-content li { margin-bottom: 3px; } | |
| .msg-content h1, .msg-content h2, .msg-content h3 { | |
| color: var(--cyan); | |
| margin: 10px 0 6px; | |
| font-size: 14px; | |
| text-shadow: var(--glow-cyan); | |
| } | |
| .msg-content h1 { font-size: 16px; } | |
| .msg-content h2 { font-size: 15px; } | |
| .msg-content p { margin: 4px 0; } | |
| /* Code blocks */ | |
| .code-block-wrap { | |
| margin: 8px 0; | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| background: var(--bg-code); | |
| } | |
| .code-block-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 4px 10px; | |
| background: rgba(30,42,58,0.5); | |
| border-bottom: 1px solid var(--border); | |
| font-size: 11px; | |
| } | |
| .code-lang { | |
| color: var(--amber); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .btn-copy { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--gray-mid); | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| padding: 2px 8px; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| } | |
| .btn-copy:hover { | |
| border-color: var(--green); | |
| color: var(--green); | |
| } | |
| .btn-copy.copied { | |
| border-color: var(--success); | |
| color: var(--success); | |
| } | |
| .code-block-wrap pre { | |
| margin: 0; | |
| padding: 10px 12px; | |
| overflow-x: auto; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| color: var(--code-text); | |
| } | |
| .code-block-wrap pre code { | |
| font-family: var(--font-mono); | |
| background: none; | |
| border: none; | |
| padding: 0; | |
| } | |
| /* Thinking blocks */ | |
| .think-block { | |
| margin: 8px 0; | |
| border: 1px solid rgba(255,179,0,0.15); | |
| border-radius: var(--radius); | |
| background: rgba(255,179,0,0.03); | |
| } | |
| .think-block summary { | |
| padding: 6px 10px; | |
| cursor: pointer; | |
| font-size: 12px; | |
| color: var(--gray-dim); | |
| user-select: none; | |
| transition: color var(--transition); | |
| } | |
| .think-block summary:hover { color: var(--amber); } | |
| .think-block .think-content { | |
| padding: 6px 12px 10px; | |
| font-size: 12px; | |
| color: var(--gray-dim); | |
| line-height: 1.55; | |
| border-top: 1px solid rgba(255,179,0,0.1); | |
| } | |
| /* Streaming cursor */ | |
| .streaming-cursor::after { | |
| content: '█'; | |
| animation: blink 0.8s step-end infinite; | |
| color: var(--green); | |
| margin-left: 2px; | |
| } | |
| @keyframes blink { | |
| 50% { opacity: 0; } | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| INPUT AREA | |
| ═══════════════════════════════════════════════════════ */ | |
| #input-area { | |
| flex-shrink: 0; | |
| border-top: 1px solid var(--border); | |
| background: var(--bg-panel); | |
| padding: 10px 16px 8px; | |
| } | |
| /* Target toggle */ | |
| #target-toggle { | |
| display: flex; | |
| gap: 14px; | |
| margin-bottom: 8px; | |
| align-items: center; | |
| } | |
| .target-label { | |
| font-size: 10px; | |
| color: var(--gray-dim); | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .target-btn { | |
| background: transparent; | |
| border: none; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| color: var(--gray-dim); | |
| cursor: pointer; | |
| padding: 2px 0; | |
| border-bottom: 2px solid transparent; | |
| transition: all var(--transition); | |
| letter-spacing: 0.5px; | |
| } | |
| .target-btn:hover { color: var(--gray-mid); } | |
| .target-btn.active { | |
| color: var(--green); | |
| border-bottom-color: var(--green); | |
| text-shadow: var(--glow-green); | |
| } | |
| #input-row { | |
| display: flex; | |
| gap: 8px; | |
| align-items: flex-end; | |
| } | |
| .input-prompt-symbol { | |
| color: var(--green); | |
| font-weight: 700; | |
| font-size: 14px; | |
| line-height: 36px; | |
| text-shadow: var(--glow-green); | |
| flex-shrink: 0; | |
| } | |
| #chat-input { | |
| flex: 1; | |
| background: var(--bg-deep); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| color: var(--green); | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| padding: 8px 12px; | |
| resize: none; | |
| outline: none; | |
| min-height: 36px; | |
| max-height: 120px; | |
| line-height: 1.5; | |
| transition: border-color var(--transition); | |
| caret-color: var(--green); | |
| text-shadow: var(--glow-green); | |
| } | |
| #chat-input::placeholder { | |
| color: var(--gray-dim); | |
| text-shadow: none; | |
| } | |
| #chat-input:focus { | |
| border-color: var(--border-focus); | |
| } | |
| #btn-send, #btn-stop { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| padding: 8px 14px; | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| letter-spacing: 1px; | |
| flex-shrink: 0; | |
| height: 36px; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| #btn-send { | |
| background: transparent; | |
| border: 1px solid var(--green); | |
| color: var(--green); | |
| } | |
| #btn-send:hover:not(:disabled) { | |
| background: var(--green); | |
| color: var(--bg-deep); | |
| box-shadow: 0 0 12px rgba(57,255,20,0.3); | |
| } | |
| #btn-send:disabled { | |
| opacity: 0.3; | |
| cursor: not-allowed; | |
| } | |
| #btn-stop { | |
| background: transparent; | |
| border: 1px solid var(--red); | |
| color: var(--red); | |
| display: none; | |
| } | |
| #btn-stop:hover { | |
| background: var(--red); | |
| color: var(--bg-deep); | |
| box-shadow: 0 0 12px rgba(255,85,85,0.3); | |
| } | |
| /* Examples */ | |
| #examples-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .examples-label { | |
| font-size: 10px; | |
| color: var(--gray-dim); | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| flex-shrink: 0; | |
| } | |
| .example-chip { | |
| background: rgba(30,42,58,0.4); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 3px 10px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--gray-mid); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| white-space: nowrap; | |
| } | |
| .example-chip:hover { | |
| border-color: var(--cyan); | |
| color: var(--cyan); | |
| background: rgba(0,212,255,0.05); | |
| text-shadow: var(--glow-cyan); | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| OUTPUT PANEL (RIGHT) | |
| ═══════════════════════════════════════════════════════ */ | |
| #output-panel { | |
| display: flex; | |
| flex-direction: column; | |
| width: 45%; | |
| min-width: 340px; | |
| max-width: 55%; | |
| background: var(--bg-panel); | |
| } | |
| #output-tabs { | |
| display: flex; | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(13,17,23,0.6); | |
| flex-shrink: 0; | |
| } | |
| .output-tab { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| border-bottom: 2px solid transparent; | |
| color: var(--gray-dim); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| padding: 8px 12px; | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .output-tab:hover { color: var(--gray-mid); } | |
| .output-tab.active { | |
| color: var(--cyan); | |
| border-bottom-color: var(--cyan); | |
| text-shadow: var(--glow-cyan); | |
| } | |
| #output-content { | |
| flex: 1; | |
| overflow: auto; | |
| position: relative; | |
| } | |
| /* Tab panes */ | |
| .tab-pane { display: none; height: 100%; } | |
| .tab-pane.active { display: flex; flex-direction: column; } | |
| /* Preview tab */ | |
| #pane-preview { | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| } | |
| .preview-placeholder { | |
| text-align: center; | |
| color: var(--gray-dim); | |
| padding: 40px 20px; | |
| } | |
| .preview-placeholder .ascii-art { | |
| font-size: 11px; | |
| line-height: 1.3; | |
| margin-bottom: 16px; | |
| color: var(--border-focus); | |
| } | |
| .preview-placeholder .placeholder-text { | |
| font-size: 12px; | |
| letter-spacing: 0.5px; | |
| } | |
| #preview-image { | |
| display: none; | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| padding: 12px; | |
| } | |
| #preview-iframe { | |
| display: none; | |
| width: 100%; | |
| flex: 1; | |
| min-height: 680px; | |
| border: none; | |
| background: #fff; | |
| } | |
| #btn-fullscreen { | |
| display: none; | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| background: rgba(13,17,23,0.8); | |
| border: 1px solid var(--border); | |
| color: var(--gray-mid); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| padding: 4px 10px; | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| z-index: 5; | |
| transition: all var(--transition); | |
| } | |
| #btn-fullscreen:hover { | |
| border-color: var(--cyan); | |
| color: var(--cyan); | |
| } | |
| /* Console tab */ | |
| #pane-console { padding: 12px 16px; gap: 12px; overflow-y: auto; } | |
| .console-section { margin-bottom: 8px; } | |
| .console-label { | |
| font-size: 10px; | |
| letter-spacing: 2px; | |
| color: var(--gray-dim); | |
| margin-bottom: 4px; | |
| text-transform: uppercase; | |
| } | |
| .console-output { | |
| background: var(--bg-deep); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 10px 12px; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| min-height: 40px; | |
| max-height: 280px; | |
| overflow-y: auto; | |
| } | |
| #console-stdout { color: var(--success); } | |
| #console-stderr { color: var(--red); } | |
| /* Code tab */ | |
| #pane-code { padding: 0; } | |
| .code-tab-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 12px; | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(30,42,58,0.3); | |
| flex-shrink: 0; | |
| } | |
| .code-tab-lang { | |
| font-size: 11px; | |
| color: var(--amber); | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .code-tab-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .code-tab-btn { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--gray-mid); | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| padding: 3px 8px; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| text-decoration: none; | |
| transition: all var(--transition); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .code-tab-btn:hover { | |
| border-color: var(--cyan); | |
| color: var(--cyan); | |
| text-decoration: none; | |
| } | |
| #code-display { | |
| flex: 1; | |
| overflow: auto; | |
| padding: 12px; | |
| background: var(--bg-code); | |
| } | |
| #code-display pre { | |
| margin: 0; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| color: var(--code-text); | |
| } | |
| .code-placeholder { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| color: var(--gray-dim); | |
| font-size: 12px; | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| STATUS BAR | |
| ═══════════════════════════════════════════════════════ */ | |
| #status-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 5px 16px; | |
| border-top: 1px solid var(--border); | |
| background: var(--bg-panel); | |
| font-size: 11px; | |
| flex-shrink: 0; | |
| } | |
| .status-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .status-dot { | |
| font-size: 10px; | |
| line-height: 1; | |
| } | |
| #status-text { | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .status-idle { color: var(--gray-dim); } | |
| .status-working { color: var(--amber); text-shadow: var(--glow-amber); } | |
| .status-success { color: var(--success); text-shadow: 0 0 8px rgba(80,250,123,0.3); } | |
| .status-error { color: var(--red); text-shadow: 0 0 8px rgba(255,85,85,0.3); } | |
| .status-info { color: var(--cyan); text-shadow: var(--glow-cyan); } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .status-working .status-dot { | |
| display: inline-block; | |
| animation: spin 1s linear infinite; | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| FULLSCREEN OVERLAY | |
| ═══════════════════════════════════════════════════════ */ | |
| #fullscreen-overlay { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| z-index: 1000; | |
| background: var(--bg-deep); | |
| flex-direction: column; | |
| } | |
| #fullscreen-overlay.active { display: flex; } | |
| #fullscreen-bar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 16px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg-panel); | |
| } | |
| #fullscreen-bar span { | |
| color: var(--cyan); | |
| font-size: 12px; | |
| letter-spacing: 1px; | |
| } | |
| #btn-exit-fullscreen { | |
| background: transparent; | |
| border: 1px solid var(--border); | |
| color: var(--gray-mid); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| padding: 4px 12px; | |
| border-radius: var(--radius); | |
| cursor: pointer; | |
| transition: all var(--transition); | |
| } | |
| #btn-exit-fullscreen:hover { | |
| border-color: var(--red); | |
| color: var(--red); | |
| } | |
| #fullscreen-iframe { | |
| flex: 1; | |
| border: none; | |
| background: #fff; | |
| } | |
| /* ═══════════════════════════════════════════════════════ | |
| RESPONSIVE | |
| ═══════════════════════════════════════════════════════ */ | |
| @media (max-width: 900px) { | |
| #main { | |
| flex-direction: column; | |
| } | |
| #terminal-panel { | |
| border-right: none; | |
| border-bottom: 1px solid var(--border); | |
| max-height: 55vh; | |
| } | |
| #output-panel { | |
| width: 100%; | |
| max-width: 100%; | |
| min-width: 0; | |
| flex: 1; | |
| } | |
| .header-ascii { font-size: 10px; } | |
| #chat-input { font-size: 12px; } | |
| #preview-iframe { min-height: 400px; } | |
| } | |
| @media (max-width: 600px) { | |
| #header { padding: 8px 12px; gap: 8px; } | |
| .header-ascii { display: none; } | |
| .header-subtitle { display: none; } | |
| .pill { font-size: 10px; padding: 3px 8px; } | |
| #chat-messages { padding: 10px; } | |
| #input-area { padding: 8px 10px 6px; } | |
| #examples-row { display: none; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <!-- Config Warning --> | |
| <div id="config-warning">⚠ WARNING: COHERE_API_KEY not configured. Set it as a Space secret to enable the model.</div> | |
| <!-- Header --> | |
| <header id="header"> | |
| <div class="header-title"> | |
| <div class="header-ascii" id="header-ascii-art">╔═══ NORTH MINI CODE 1.0 ═══╗</div> | |
| <div class="header-subtitle" id="header-subtitle">Terminal Coding Assistant</div> | |
| </div> | |
| <div class="header-actions"> | |
| <a class="pill" id="model-pill" href="#" target="_blank" rel="noopener"> | |
| <span class="dot"></span> | |
| <span id="model-pill-text">north-mini-code-1-0</span> | |
| </a> | |
| <a class="pill" id="opencode-pill" href="#" target="_blank" rel="noopener">OpenCode</a> | |
| <button id="btn-new-chat" onclick="newChat()" title="Start a new chat session">[NEW]</button> | |
| </div> | |
| </header> | |
| <!-- Main Layout --> | |
| <div id="main"> | |
| <!-- Terminal Panel --> | |
| <div id="terminal-panel"> | |
| <div class="panel-label">Terminal</div> | |
| <div id="chat-messages"></div> | |
| <div id="input-area"> | |
| <div id="target-toggle"> | |
| <span class="target-label">Mode:</span> | |
| <button class="target-btn active" data-target="Python" onclick="setTarget('Python')">[ Python ● ]</button> | |
| <button class="target-btn" data-target="Web" onclick="setTarget('Web')">[ Web ○ ]</button> | |
| </div> | |
| <div id="input-row"> | |
| <span class="input-prompt-symbol">❯</span> | |
| <textarea id="chat-input" rows="1" placeholder="Ask a coding question or describe what to build…" spellcheck="false"></textarea> | |
| <button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">➤</button> | |
| <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">■ STOP</button> | |
| </div> | |
| <div id="examples-row"></div> | |
| </div> | |
| </div> | |
| <!-- Output Panel --> | |
| <div id="output-panel"> | |
| <div id="output-tabs"> | |
| <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button> | |
| <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button> | |
| <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button> | |
| </div> | |
| <div id="output-content"> | |
| <!-- Preview Pane --> | |
| <div class="tab-pane active" id="pane-preview"> | |
| <div class="preview-placeholder" id="preview-placeholder"> | |
| <div class="ascii-art"> | |
| ┌──────────────────────┐ | |
| │ ╭━━━╮ │ | |
| │ ┃ ▶ ┃ OUTPUT │ | |
| │ ╰━━━╯ │ | |
| └──────────────────────┘</div> | |
| <div class="placeholder-text">Run code to see output here</div> | |
| </div> | |
| <img id="preview-image" alt="Generated output"> | |
| <iframe id="preview-iframe" sandbox="allow-scripts"></iframe> | |
| <button id="btn-fullscreen" onclick="openFullscreen()">⤢ FULLSCREEN</button> | |
| </div> | |
| <!-- Console Pane --> | |
| <div class="tab-pane" id="pane-console"> | |
| <div class="console-section"> | |
| <div class="console-label">stdout:</div> | |
| <div class="console-output" id="console-stdout">No output yet.</div> | |
| </div> | |
| <div class="console-section"> | |
| <div class="console-label">stderr:</div> | |
| <div class="console-output" id="console-stderr">No errors.</div> | |
| </div> | |
| </div> | |
| <!-- Code Pane --> | |
| <div class="tab-pane" id="pane-code"> | |
| <div class="code-tab-header"> | |
| <span class="code-tab-lang" id="code-tab-lang">—</span> | |
| <div class="code-tab-actions"> | |
| <button class="code-tab-btn" id="btn-copy-code" onclick="copyCode()">📋 Copy</button> | |
| <a class="code-tab-btn" id="btn-download" href="#" style="display:none;">⬇ Download</a> | |
| </div> | |
| </div> | |
| <div id="code-display"> | |
| <div class="code-placeholder">No code generated yet.</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Status Bar --> | |
| <div id="status-bar"> | |
| <div class="status-indicator status-idle" id="status-indicator"> | |
| <span class="status-dot">●</span> | |
| <span id="status-text">IDLE</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Fullscreen Overlay --> | |
| <div id="fullscreen-overlay"> | |
| <div id="fullscreen-bar"> | |
| <span>WEB PREVIEW</span> | |
| <button id="btn-exit-fullscreen" onclick="closeFullscreen()">[✕ CLOSE]</button> | |
| </div> | |
| <iframe id="fullscreen-iframe" sandbox="allow-scripts"></iframe> | |
| </div> | |
| <script> | |
| // ═══════════════════════════════════════════════════════ | |
| // CONFIG | |
| // ═══════════════════════════════════════════════════════ | |
| const CONFIG = __RUNTIME_CONFIG__; | |
| // ═══════════════════════════════════════════════════════ | |
| // STATE | |
| // ═══════════════════════════════════════════════════════ | |
| const state = { | |
| history: [], | |
| executionContext: {}, | |
| targetLanguage: 'Python', | |
| isGenerating: false, | |
| currentEventSource: null, | |
| activeTab: 'preview', | |
| lastExecution: null, | |
| lastCode: '', | |
| lastCodeLang: '' | |
| }; | |
| // ═══════════════════════════════════════════════════════ | |
| // INITIALIZATION | |
| // ═══════════════════════════════════════════════════════ | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Apply config | |
| document.title = CONFIG.app_title || 'North Mini Code 1.0'; | |
| if (CONFIG.model_url) { | |
| document.getElementById('model-pill').href = CONFIG.model_url; | |
| } | |
| if (CONFIG.model_id) { | |
| document.getElementById('model-pill-text').textContent = CONFIG.model_id; | |
| } | |
| if (CONFIG.opencode_url) { | |
| document.getElementById('opencode-pill').href = CONFIG.opencode_url; | |
| } | |
| // Config warning | |
| if (!CONFIG.api_key_configured) { | |
| document.getElementById('config-warning').classList.add('visible'); | |
| } | |
| // Render examples | |
| renderExamples(); | |
| // Welcome message | |
| addSystemMessage(`Welcome to ${CONFIG.app_title || 'North Mini Code 1.0'}. Type a coding question or select an example below to get started.`); | |
| // Input auto-resize & keybinding | |
| const input = document.getElementById('chat-input'); | |
| input.addEventListener('input', autoResize); | |
| input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && e.shiftKey) { | |
| e.preventDefault(); | |
| handleSend(); | |
| } | |
| }); | |
| }); | |
| function autoResize() { | |
| const el = document.getElementById('chat-input'); | |
| el.style.height = 'auto'; | |
| el.style.height = Math.min(el.scrollHeight, 120) + 'px'; | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // EXAMPLES | |
| // ═══════════════════════════════════════════════════════ | |
| function renderExamples() { | |
| const row = document.getElementById('examples-row'); | |
| if (!CONFIG.examples || CONFIG.examples.length === 0) { | |
| row.style.display = 'none'; | |
| return; | |
| } | |
| row.innerHTML = '<span class="examples-label">Try:</span>'; | |
| CONFIG.examples.forEach((ex) => { | |
| const chip = document.createElement('button'); | |
| chip.className = 'example-chip'; | |
| chip.textContent = ex.label; | |
| chip.title = ex.prompt; | |
| chip.addEventListener('click', () => { | |
| if (ex.target) setTarget(ex.target); | |
| sendMessage(ex.prompt); | |
| }); | |
| row.appendChild(chip); | |
| }); | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // TARGET LANGUAGE | |
| // ═══════════════════════════════════════════════════════ | |
| function setTarget(target) { | |
| state.targetLanguage = target; | |
| document.querySelectorAll('.target-btn').forEach((btn) => { | |
| const isActive = btn.dataset.target === target; | |
| btn.classList.toggle('active', isActive); | |
| btn.textContent = `[ ${btn.dataset.target} ${isActive ? '●' : '○'} ]`; | |
| }); | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // CHAT MESSAGES | |
| // ═══════════════════════════════════════════════════════ | |
| function addSystemMessage(text) { | |
| const container = document.getElementById('chat-messages'); | |
| const div = document.createElement('div'); | |
| div.className = 'msg msg-system'; | |
| div.innerHTML = `<span class="msg-prefix">system></span><span class="msg-content">${escapeHtml(text)}</span>`; | |
| container.appendChild(div); | |
| scrollToBottom(); | |
| } | |
| function addUserMessage(text) { | |
| const container = document.getElementById('chat-messages'); | |
| const div = document.createElement('div'); | |
| div.className = 'msg msg-user'; | |
| div.innerHTML = `<span class="msg-prefix">user></span><span class="msg-content">${escapeHtml(text)}</span>`; | |
| container.appendChild(div); | |
| scrollToBottom(); | |
| } | |
| function addAssistantMessage() { | |
| const container = document.getElementById('chat-messages'); | |
| const div = document.createElement('div'); | |
| div.className = 'msg msg-assistant'; | |
| div.id = 'current-assistant-msg'; | |
| div.innerHTML = `<span class="msg-prefix">north></span><span class="msg-content streaming-cursor"></span>`; | |
| container.appendChild(div); | |
| scrollToBottom(); | |
| return div; | |
| } | |
| function updateAssistantMessage(content, isStreaming) { | |
| const div = document.getElementById('current-assistant-msg'); | |
| if (!div) return; | |
| const contentEl = div.querySelector('.msg-content'); | |
| contentEl.innerHTML = parseMarkdown(content); | |
| if (isStreaming) { | |
| contentEl.classList.add('streaming-cursor'); | |
| } else { | |
| contentEl.classList.remove('streaming-cursor'); | |
| } | |
| scrollToBottom(); | |
| } | |
| function finalizeAssistantMessage() { | |
| const div = document.getElementById('current-assistant-msg'); | |
| if (div) { | |
| div.id = ''; | |
| const contentEl = div.querySelector('.msg-content'); | |
| if (contentEl) contentEl.classList.remove('streaming-cursor'); | |
| } | |
| } | |
| function scrollToBottom() { | |
| const container = document.getElementById('chat-messages'); | |
| requestAnimationFrame(() => { | |
| container.scrollTop = container.scrollHeight; | |
| }); | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // MARKDOWN PARSER | |
| // ═══════════════════════════════════════════════════════ | |
| function parseMarkdown(text) { | |
| if (!text) return ''; | |
| // Handle think blocks | |
| text = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => { | |
| return `<details class="think-block"><summary>💭 Reasoning (click to expand)</summary><div class="think-content">${escapeHtml(content.trim())}</div></details>`; | |
| }); | |
| // Handle unclosed think blocks (during streaming) | |
| text = text.replace(/<think>([\s\S]*)$/g, (_, content) => { | |
| return `<details class="think-block"><summary>💭 Reasoning (thinking…)</summary><div class="think-content">${escapeHtml(content.trim())}</div></details>`; | |
| }); | |
| // Extract code blocks first to prevent inner processing | |
| const codeBlocks = []; | |
| text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => { | |
| const idx = codeBlocks.length; | |
| codeBlocks.push({ lang: lang || 'text', code: code.trimEnd() }); | |
| return `\x00CODEBLOCK_${idx}\x00`; | |
| }); | |
| // Escape HTML in remaining text | |
| text = escapeHtml(text); | |
| // Restore code blocks with formatting | |
| text = text.replace(/\x00CODEBLOCK_(\d+)\x00/g, (_, idx) => { | |
| const block = codeBlocks[parseInt(idx)]; | |
| const escapedCode = escapeHtml(block.code); | |
| const id = `code-${Date.now()}-${idx}`; | |
| return `<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">${escapeHtml(block.lang)}</span><button class="btn-copy" onclick="copyBlock(this, '${id}')">📋 Copy</button></div><pre><code id="${id}">${escapedCode}</code></pre></div>`; | |
| }); | |
| // Inline formatting | |
| text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); | |
| text = text.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>'); | |
| text = text.replace(/`([^`]+?)`/g, '<code>$1</code>'); | |
| text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>'); | |
| // Headings | |
| text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>'); | |
| text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>'); | |
| text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>'); | |
| // Lists - simple approach | |
| // Unordered | |
| text = text.replace(/^(?:[-*]) (.+)$/gm, '<li>$1</li>'); | |
| text = text.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>'); | |
| // Ordered | |
| text = text.replace(/^\d+\. (.+)$/gm, '<li>$1</li>'); | |
| // Wrap consecutive <li> that aren't in <ul> into <ol> | |
| text = text.replace(/(<li>(?:(?!<\/?[uo]l>).)*<\/li>(?:\s*<li>(?:(?!<\/?[uo]l>).)*<\/li>)*)/g, (match) => { | |
| if (!match.includes('<ul>') && !match.includes('</ul>')) { | |
| return '<ol>' + match + '</ol>'; | |
| } | |
| return match; | |
| }); | |
| // Line breaks (preserve paragraph structure) | |
| text = text.replace(/\n\n/g, '</p><p>'); | |
| text = text.replace(/\n/g, '<br>'); | |
| text = '<p>' + text + '</p>'; | |
| // Clean up empty paragraphs | |
| text = text.replace(/<p>\s*<\/p>/g, ''); | |
| text = text.replace(/<p>(<(?:div|ul|ol|h[1-3]|details))/g, '$1'); | |
| text = text.replace(/(<\/(?:div|ul|ol|h[1-3]|details)>)<\/p>/g, '$1'); | |
| return text; | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // COPY FUNCTIONS | |
| // ═══════════════════════════════════════════════════════ | |
| function copyBlock(button, codeId) { | |
| const codeEl = document.getElementById(codeId); | |
| if (!codeEl) return; | |
| navigator.clipboard.writeText(codeEl.textContent).then(() => { | |
| button.textContent = '✓ Copied!'; | |
| button.classList.add('copied'); | |
| setTimeout(() => { | |
| button.textContent = '📋 Copy'; | |
| button.classList.remove('copied'); | |
| }, 2000); | |
| }); | |
| } | |
| function copyCode() { | |
| if (!state.lastCode) return; | |
| const btn = document.getElementById('btn-copy-code'); | |
| navigator.clipboard.writeText(state.lastCode).then(() => { | |
| btn.textContent = '✓ Copied!'; | |
| setTimeout(() => { btn.textContent = '📋 Copy'; }, 2000); | |
| }); | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // STATUS BAR | |
| // ═══════════════════════════════════════════════════════ | |
| function renderStatus(text, statusState) { | |
| const indicator = document.getElementById('status-indicator'); | |
| const textEl = document.getElementById('status-text'); | |
| const dotEl = indicator.querySelector('.status-dot'); | |
| indicator.className = 'status-indicator'; | |
| switch (statusState) { | |
| case 'working': | |
| indicator.classList.add('status-working'); | |
| dotEl.textContent = '◐'; | |
| break; | |
| case 'success': | |
| indicator.classList.add('status-success'); | |
| dotEl.textContent = '✓'; | |
| break; | |
| case 'error': | |
| indicator.classList.add('status-error'); | |
| dotEl.textContent = '✗'; | |
| break; | |
| case 'info': | |
| indicator.classList.add('status-info'); | |
| dotEl.textContent = 'ℹ'; | |
| break; | |
| default: | |
| indicator.classList.add('status-idle'); | |
| dotEl.textContent = '●'; | |
| } | |
| textEl.textContent = text || 'IDLE'; | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // OUTPUT PANEL | |
| // ═══════════════════════════════════════════════════════ | |
| function switchTab(tab) { | |
| state.activeTab = tab; | |
| document.querySelectorAll('.output-tab').forEach((btn) => { | |
| btn.classList.toggle('active', btn.dataset.tab === tab); | |
| }); | |
| document.querySelectorAll('.tab-pane').forEach((pane) => { | |
| pane.classList.toggle('active', pane.id === `pane-${tab}`); | |
| }); | |
| } | |
| function renderExecution(execution) { | |
| if (!execution) return; | |
| state.lastExecution = execution; | |
| // Console | |
| const stdout = execution.stdout || ''; | |
| const stderr = execution.stderr || ''; | |
| document.getElementById('console-stdout').textContent = stdout || 'No output.'; | |
| document.getElementById('console-stderr').textContent = stderr || 'No errors.'; | |
| // Code | |
| if (execution.code) { | |
| state.lastCode = execution.code; | |
| state.lastCodeLang = execution.language || 'code'; | |
| document.getElementById('code-tab-lang').textContent = state.lastCodeLang; | |
| document.getElementById('code-display').innerHTML = `<pre>${escapeHtml(execution.code)}</pre>`; | |
| } | |
| // Download | |
| const dlBtn = document.getElementById('btn-download'); | |
| if (execution.download_url) { | |
| dlBtn.href = execution.download_url; | |
| dlBtn.style.display = 'inline-flex'; | |
| dlBtn.setAttribute('download', ''); | |
| } else { | |
| dlBtn.style.display = 'none'; | |
| } | |
| // Preview | |
| const placeholder = document.getElementById('preview-placeholder'); | |
| const img = document.getElementById('preview-image'); | |
| const iframe = document.getElementById('preview-iframe'); | |
| const fsBtn = document.getElementById('btn-fullscreen'); | |
| if (execution.image_url) { | |
| placeholder.style.display = 'none'; | |
| iframe.style.display = 'none'; | |
| fsBtn.style.display = 'none'; | |
| img.src = execution.image_url; | |
| img.style.display = 'block'; | |
| if (state.activeTab !== 'console' && state.activeTab !== 'code') { | |
| switchTab('preview'); | |
| } | |
| } else if (execution.target === 'web' && execution.code) { | |
| placeholder.style.display = 'none'; | |
| img.style.display = 'none'; | |
| iframe.srcdoc = execution.code; | |
| iframe.style.display = 'block'; | |
| fsBtn.style.display = 'block'; | |
| if (state.activeTab !== 'console' && state.activeTab !== 'code') { | |
| switchTab('preview'); | |
| } | |
| } else { | |
| // No visual preview, but maybe there's stdout | |
| if (stdout || stderr) { | |
| // Auto-switch to suggested tab or console | |
| const suggested = execution.suggested_tab || 'console'; | |
| switchTab(suggested); | |
| } | |
| } | |
| } | |
| function resetOutput() { | |
| document.getElementById('preview-placeholder').style.display = ''; | |
| document.getElementById('preview-image').style.display = 'none'; | |
| document.getElementById('preview-iframe').style.display = 'none'; | |
| document.getElementById('preview-iframe').srcdoc = ''; | |
| document.getElementById('btn-fullscreen').style.display = 'none'; | |
| document.getElementById('console-stdout').textContent = 'No output.'; | |
| document.getElementById('console-stderr').textContent = 'No errors.'; | |
| document.getElementById('code-display').innerHTML = '<div class="code-placeholder">No code generated yet.</div>'; | |
| document.getElementById('code-tab-lang').textContent = '—'; | |
| document.getElementById('btn-download').style.display = 'none'; | |
| state.lastExecution = null; | |
| state.lastCode = ''; | |
| state.lastCodeLang = ''; | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // FULLSCREEN | |
| // ═══════════════════════════════════════════════════════ | |
| function openFullscreen() { | |
| const overlay = document.getElementById('fullscreen-overlay'); | |
| const iframe = document.getElementById('fullscreen-iframe'); | |
| if (state.lastExecution && state.lastExecution.target === 'web' && state.lastExecution.code) { | |
| iframe.srcdoc = state.lastExecution.code; | |
| } | |
| overlay.classList.add('active'); | |
| } | |
| function closeFullscreen() { | |
| document.getElementById('fullscreen-overlay').classList.remove('active'); | |
| document.getElementById('fullscreen-iframe').srcdoc = ''; | |
| } | |
| // ═══════════════════════════════════════════════════════ | |
| // SEND / RECEIVE | |
| // ═══════════════════════════════════════════════════════ | |
| function handleSend() { | |
| const input = document.getElementById('chat-input'); | |
| const prompt = input.value.trim(); | |
| if (!prompt || state.isGenerating) return; | |
| input.value = ''; | |
| autoResize(); | |
| sendMessage(prompt); | |
| } | |
| async function sendMessage(prompt) { | |
| if (state.isGenerating) return; | |
| // UI updates | |
| state.isGenerating = true; | |
| toggleInputState(true); | |
| addUserMessage(prompt); | |
| addAssistantMessage(); | |
| renderStatus('Thinking…', 'working'); | |
| const historyJSON = JSON.stringify(state.history); | |
| const execContextJSON = JSON.stringify(state.executionContext); | |
| try { | |
| // Step 1: POST to get event_id | |
| const resp = await fetch('/gradio_api/call/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| data: [prompt, state.targetLanguage, historyJSON, execContextJSON] | |
| }) | |
| }); | |
| if (!resp.ok) { | |
| throw new Error(`API error: ${resp.status} ${resp.statusText}`); | |
| } | |
| const { event_id } = await resp.json(); | |
| // Step 2: Connect EventSource | |
| const eventSource = new EventSource(`/gradio_api/call/chat/${event_id}`); | |
| state.currentEventSource = eventSource; | |
| let lastContent = ''; | |
| eventSource.addEventListener('generating', (e) => { | |
| try { | |
| const dataArray = JSON.parse(e.data); | |
| const payload = JSON.parse(dataArray[0]); | |
| handlePayload(payload, true); | |
| if (payload.history && payload.history.length > 0) { | |
| const lastMsg = payload.history[payload.history.length - 1]; | |
| if (lastMsg.role === 'assistant') { | |
| lastContent = lastMsg.content; | |
| } | |
| } | |
| } catch (err) { | |
| console.error('Parse error (generating):', err); | |
| } | |
| }); | |
| eventSource.addEventListener('complete', (e) => { | |
| try { | |
| const dataArray = JSON.parse(e.data); | |
| const payload = JSON.parse(dataArray[0]); | |
| handlePayload(payload, false); | |
| } catch (err) { | |
| console.error('Parse error (complete):', err); | |
| } | |
| eventSource.close(); | |
| onGenerationEnd(); | |
| }); | |
| eventSource.addEventListener('error', (e) => { | |
| let errorMsg = 'An error occurred during generation.'; | |
| if (e.data) { | |
| errorMsg = e.data; | |
| } | |
| console.error('SSE error:', errorMsg); | |
| finalizeAssistantMessage(); | |
| addSystemMessage(`Error: ${errorMsg}`); | |
| renderStatus('Error', 'error'); | |
| eventSource.close(); | |
| onGenerationEnd(); | |
| }); | |
| } catch (err) { | |
| console.error('Send error:', err); | |
| finalizeAssistantMessage(); | |
| addSystemMessage(`Error: ${err.message}`); | |
| renderStatus('Error', 'error'); | |
| onGenerationEnd(); | |
| } | |
| } | |
| function handlePayload(payload, isStreaming) { | |
| // Status | |
| if (payload.status_text) { | |
| renderStatus(payload.status_text, payload.status_state || 'working'); | |
| } | |
| // History | |
| if (payload.history) { | |
| state.history = payload.history; | |
| const lastMsg = payload.history[payload.history.length - 1]; | |
| if (lastMsg && lastMsg.role === 'assistant') { | |
| updateAssistantMessage(lastMsg.content, isStreaming); | |
| } | |
| } | |
| // Execution | |
| if (payload.execution) { | |
| renderExecution(payload.execution); | |
| // Persist execution context if needed | |
| if (payload.execution) { | |
| state.executionContext = payload.execution; | |
| } | |
| } | |
| // Final state | |
| if (payload.type === 'complete') { | |
| finalizeAssistantMessage(); | |
| renderStatus('Done', 'success'); | |
| setTimeout(() => { | |
| if (!state.isGenerating) renderStatus('Idle', 'idle'); | |
| }, 3000); | |
| } | |
| if (payload.type === 'error') { | |
| finalizeAssistantMessage(); | |
| addSystemMessage(`Error: ${payload.status_text || 'Unknown error'}`); | |
| renderStatus('Error', 'error'); | |
| } | |
| // Auto-switch tab | |
| if (payload.execution && payload.execution.suggested_tab) { | |
| switchTab(payload.execution.suggested_tab); | |
| } | |
| } | |
| function onGenerationEnd() { | |
| state.isGenerating = false; | |
| state.currentEventSource = null; | |
| toggleInputState(false); | |
| } | |
| function toggleInputState(generating) { | |
| const sendBtn = document.getElementById('btn-send'); | |
| const stopBtn = document.getElementById('btn-stop'); | |
| const input = document.getElementById('chat-input'); | |
| if (generating) { | |
| sendBtn.style.display = 'none'; | |
| stopBtn.style.display = 'flex'; | |
| input.disabled = true; | |
| input.placeholder = 'Generating…'; | |
| } else { | |
| sendBtn.style.display = 'flex'; | |
| stopBtn.style.display = 'none'; | |
| sendBtn.disabled = false; | |
| input.disabled = false; | |
| input.placeholder = 'Ask a coding question or describe what to build…'; | |
| input.focus(); | |
| } | |
| } | |
| function stopGeneration() { | |
| if (state.currentEventSource) { | |
| state.currentEventSource.close(); | |
| state.currentEventSource = null; | |
| } | |
| finalizeAssistantMessage(); | |
| addSystemMessage('Generation stopped by user.'); | |
| renderStatus('Stopped', 'info'); | |
| onGenerationEnd(); | |
| } | |
| function newChat() { | |
| state.history = []; | |
| state.executionContext = {}; | |
| state.lastExecution = null; | |
| state.lastCode = ''; | |
| state.lastCodeLang = ''; | |
| if (state.currentEventSource) { | |
| state.currentEventSource.close(); | |
| state.currentEventSource = null; | |
| } | |
| state.isGenerating = false; | |
| toggleInputState(false); | |
| document.getElementById('chat-messages').innerHTML = ''; | |
| resetOutput(); | |
| switchTab('preview'); | |
| renderStatus('Idle', 'idle'); | |
| addSystemMessage(`Session reset. Welcome back to ${CONFIG.app_title || 'North Mini Code 1.0'}.`); | |
| } | |
| </script> | |
| </body> | |
| </html> | |