North-Mini-Code-1.0 / index.html
akhaliq's picture
akhaliq HF Staff
Migrate to terminal chat UI using gradio.Server and custom frontend
4536743
<!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>
/* ═══════════════════════════════════════════════════════
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&gt;</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&gt;</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&gt;</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>