| <!DOCTYPE html> |
| <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> |
| |
| |
| |
| *, *::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; |
| } |
| |
| |
| 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 { |
| display: flex; |
| flex-direction: column; |
| height: 100vh; |
| max-height: 100vh; |
| } |
| |
| |
| |
| |
| #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 { |
| 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 { |
| display: flex; |
| flex: 1; |
| min-height: 0; |
| overflow: hidden; |
| } |
| |
| |
| |
| |
| #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; |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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-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; |
| } |
| |
| |
| .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::after { |
| content: '█'; |
| animation: blink 0.8s step-end infinite; |
| color: var(--green); |
| margin-left: 2px; |
| } |
| @keyframes blink { |
| 50% { opacity: 0; } |
| } |
| |
| |
| |
| |
| #input-area { |
| flex-shrink: 0; |
| border-top: 1px solid var(--border); |
| background: var(--bg-panel); |
| padding: 10px 16px 8px; |
| } |
| |
| |
| #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-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 { |
| 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-pane { display: none; height: 100%; } |
| .tab-pane.active { display: flex; flex-direction: column; } |
| |
| |
| #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); |
| } |
| |
| |
| #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); } |
| |
| |
| #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 { |
| 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 { |
| 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; |
| } |
| |
| |
| |
| |
| @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"> |
| |
| <div id="config-warning">⚠ WARNING: COHERE_API_KEY not configured. Set it as a Space secret to enable the model.</div> |
|
|
| |
| <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> |
|
|
| |
| <div id="main"> |
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| |
| |
| const CONFIG = __RUNTIME_CONFIG__; |
| |
| |
| |
| |
| const state = { |
| history: [], |
| executionContext: {}, |
| targetLanguage: 'Python', |
| isGenerating: false, |
| currentEventSource: null, |
| activeTab: 'preview', |
| lastExecution: null, |
| lastCode: '', |
| lastCodeLang: '' |
| }; |
| |
| |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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; |
| } |
| |
| |
| if (!CONFIG.api_key_configured) { |
| document.getElementById('config-warning').classList.add('visible'); |
| } |
| |
| |
| renderExamples(); |
| |
| |
| addSystemMessage(`Welcome to ${CONFIG.app_title || 'North Mini Code 1.0'}. Type a coding question or select an example below to get started.`); |
| |
| |
| 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'; |
| } |
| |
| |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| |
| |
| 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 ? '●' : '○'} ]`; |
| }); |
| } |
| |
| |
| |
| |
| 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; |
| }); |
| } |
| |
| |
| |
| |
| function parseMarkdown(text) { |
| if (!text) return ''; |
| |
| |
| 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>`; |
| }); |
| |
| 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>`; |
| }); |
| |
| |
| 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`; |
| }); |
| |
| |
| text = escapeHtml(text); |
| |
| |
| 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>`; |
| }); |
| |
| |
| 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>'); |
| |
| |
| text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>'); |
| text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>'); |
| text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>'); |
| |
| |
| |
| text = text.replace(/^(?:[-*]) (.+)$/gm, '<li>$1</li>'); |
| text = text.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>'); |
| |
| text = text.replace(/^\d+\. (.+)$/gm, '<li>$1</li>'); |
| |
| 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; |
| }); |
| |
| |
| text = text.replace(/\n\n/g, '</p><p>'); |
| text = text.replace(/\n/g, '<br>'); |
| text = '<p>' + text + '</p>'; |
| |
| |
| 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; |
| } |
| |
| |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| |
| |
| 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'; |
| } |
| |
| |
| |
| |
| 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; |
| |
| |
| const stdout = execution.stdout || ''; |
| const stderr = execution.stderr || ''; |
| document.getElementById('console-stdout').textContent = stdout || 'No output.'; |
| document.getElementById('console-stderr').textContent = stderr || 'No errors.'; |
| |
| |
| 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>`; |
| } |
| |
| |
| 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'; |
| } |
| |
| |
| 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 { |
| |
| if (stdout || stderr) { |
| |
| 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 = ''; |
| } |
| |
| |
| |
| |
| 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 = ''; |
| } |
| |
| |
| |
| |
| 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; |
| |
| |
| state.isGenerating = true; |
| toggleInputState(true); |
| addUserMessage(prompt); |
| addAssistantMessage(); |
| renderStatus('Thinking…', 'working'); |
| |
| const historyJSON = JSON.stringify(state.history); |
| const execContextJSON = JSON.stringify(state.executionContext); |
| |
| try { |
| |
| 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(); |
| |
| |
| 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) { |
| |
| if (payload.status_text) { |
| renderStatus(payload.status_text, payload.status_state || 'working'); |
| } |
| |
| |
| if (payload.history) { |
| state.history = payload.history; |
| const lastMsg = payload.history[payload.history.length - 1]; |
| if (lastMsg && lastMsg.role === 'assistant') { |
| updateAssistantMessage(lastMsg.content, isStreaming); |
| } |
| } |
| |
| |
| if (payload.execution) { |
| renderExecution(payload.execution); |
| |
| if (payload.execution) { |
| state.executionContext = payload.execution; |
| } |
| } |
| |
| |
| 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'); |
| } |
| |
| |
| 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> |
|
|