sonicoder / index.html
R-Kentaren's picture
fix: agent_run param mismatch (send agent_name) + add GitHub push-update (3 inputs: repo name, token, username; --force-with-lease)
0df4996 verified
Raw
History Blame Contribute Delete
152 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SoniCoder</title>
<meta name="description" content="AI-powered fullstack app generator with local model inference">
<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&amp;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;
--purple: #a855f7;
--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);
--glow-purple: 0 0 8px rgba(168,85,247,0.3);
--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 SHELL
═══════════════════════════════════════════════════════ */
#app {
display: flex;
flex-direction: column;
height: 100vh;
max-height: 100vh;
}
/* ═══════════════════════════════════════════════════════
HEADER
═══════════════════════════════════════════════════════ */
#header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 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);
}
.pill .dot.loading {
background: var(--amber);
box-shadow: 0 0 6px var(--amber);
animation: pulse 1.5s ease infinite;
}
.pill .dot.error {
background: var(--red);
box-shadow: 0 0 6px var(--red);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
#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);
}
.btn-thinking {
background: transparent;
border: 1px solid var(--border);
color: var(--purple);
font-family: var(--font-mono);
font-size: 11px;
padding: 5px 12px;
border-radius: var(--radius);
cursor: pointer;
transition: all var(--transition);
letter-spacing: 0.5px;
}
.btn-thinking:hover {
border-color: var(--purple);
background: rgba(168,85,247,0.08);
text-shadow: var(--glow-purple);
}
.btn-thinking.active {
border-color: var(--purple);
background: rgba(168,85,247,0.15);
color: var(--purple);
text-shadow: var(--glow-purple);
}
#model-select {
background: var(--bg-deep);
border: 1px solid var(--border);
color: var(--cyan);
font-family: var(--font-mono);
font-size: 11px;
padding: 5px 8px;
border-radius: var(--radius);
outline: none;
cursor: pointer;
transition: border-color var(--transition);
}
#model-select:focus { border-color: var(--border-focus); }
#model-select option { background: var(--bg-deep); color: var(--gray-light); }
#btn-attach-image {
background: transparent; border: 1px solid var(--border); color: var(--amber);
font-family: var(--font-mono); font-size: 14px; padding: 3px 8px;
border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
}
#btn-attach-image:hover {
border-color: var(--amber); background: rgba(255,179,0,0.1);
}
/* ═══════════════════════════════════════════════════════
BANNER
═══════════════════════════════════════════════════════ */
#playground-banner {
background: linear-gradient(90deg, rgba(168,85,247,0.08), rgba(57,255,20,0.05));
border-bottom: 1px solid var(--border);
color: var(--gray-mid);
font-size: 12px;
padding: 7px 18px;
text-align: center;
flex-shrink: 0;
}
#playground-banner strong { color: var(--gray-light); font-weight: 600; }
#playground-banner a { color: var(--purple); font-weight: 600; }
/* ═══════════════════════════════════════════════════════
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); float: left; }
.msg-assistant .msg-body { overflow: hidden; }
.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-summary {
display: block; width: 100%; background: transparent; border: none;
padding: 6px 10px; cursor: pointer; font-size: 12px;
font-family: var(--font-mono); text-align: left; color: var(--gray-dim);
user-select: none; transition: color var(--transition);
}
.think-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);
}
.think-block:not(.open) .think-content { display: none; }
/* Hide thinking blocks entirely when toggle is off */
body.hide-thinking .think-block { display: none; }
/* ─── Search Source Badge (Grok-style) ────────────────────── */
.search-source-badge {
display: inline-flex;
align-items: center;
gap: 5px;
margin: 4px 0 2px;
padding: 4px 10px;
border-radius: 20px;
background: rgba(168, 85, 247, 0.08);
border: 1px solid rgba(168, 85, 247, 0.2);
cursor: pointer;
transition: all 0.2s ease;
font-size: 11px;
color: var(--purple);
user-select: none;
white-space: nowrap;
}
.search-source-badge:hover {
background: rgba(168, 85, 247, 0.15);
border-color: rgba(168, 85, 247, 0.4);
box-shadow: 0 0 8px rgba(168, 85, 247, 0.15);
}
.search-source-badge .badge-icon {
font-size: 13px;
line-height: 1;
}
.search-source-badge .badge-count {
font-weight: 600;
}
.search-source-badge .badge-arrow {
font-size: 9px;
transition: transform 0.2s ease;
opacity: 0.6;
}
.search-source-badge.open .badge-arrow {
transform: rotate(180deg);
}
.search-source-panel {
margin: 0 0 6px;
border-radius: 8px;
border: 1px solid rgba(168, 85, 247, 0.15);
background: rgba(168, 85, 247, 0.03);
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
opacity: 0;
}
.search-source-panel.open {
max-height: 600px;
overflow-y: auto;
opacity: 1;
padding: 8px 10px;
}
.search-source-panel::-webkit-scrollbar { width: 4px; }
.search-source-panel::-webkit-scrollbar-track { background: transparent; }
.search-source-panel::-webkit-scrollbar-thumb { background: var(--gray-dim); border-radius: 2px; }
.source-item {
display: flex;
gap: 8px;
padding: 6px 4px;
border-bottom: 1px solid rgba(168, 85, 247, 0.07);
align-items: flex-start;
}
.source-item:last-child { border-bottom: none; }
.source-favicon {
width: 16px; height: 16px;
border-radius: 3px;
flex-shrink: 0;
margin-top: 2px;
background: var(--bg-code);
border: 1px solid var(--border);
object-fit: contain;
}
.source-favicon-placeholder {
width: 16px; height: 16px;
border-radius: 3px;
flex-shrink: 0;
margin-top: 2px;
background: var(--bg-code);
border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
font-size: 8px; color: var(--gray-dim);
}
.source-info { flex: 1; min-width: 0; }
.source-title {
font-size: 11px; font-weight: 600; color: var(--cyan);
text-decoration: none; display: block;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.source-title:hover { text-decoration: underline; }
.source-domain {
font-size: 9px; color: var(--green-dim);
display: block; margin-top: 1px;
}
.source-snippet {
font-size: 10px; color: var(--gray-mid);
line-height: 1.35;
margin-top: 2px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Streaming cursor */
.streaming-cursor::after {
content: '\u2588'; 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 selectors */
#target-selector {
display: flex;
gap: 10px;
margin-bottom: 6px;
align-items: center;
flex-wrap: wrap;
}
.selector-group {
display: flex;
align-items: center;
gap: 6px;
}
.selector-label {
font-size: 10px;
color: var(--gray-dim);
letter-spacing: 1px;
text-transform: uppercase;
}
#lang-select, #framework-select {
background: var(--bg-deep);
border: 1px solid var(--border);
color: var(--green);
font-family: var(--font-mono);
font-size: 11px;
padding: 3px 8px;
border-radius: var(--radius);
outline: none;
cursor: pointer;
transition: border-color var(--transition);
}
#lang-select:focus, #framework-select:focus {
border-color: var(--border-focus);
}
#lang-select option, #framework-select option {
background: var(--bg-deep);
color: var(--gray-light);
}
#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-web-search {
background: transparent; border: 1px solid var(--purple); color: var(--purple);
font-family: var(--font-mono); font-size: 11px; padding: 8px 10px;
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-web-search:hover {
background: var(--purple); color: white;
box-shadow: 0 0 12px rgba(168,85,247,0.3);
text-shadow: none;
}
#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(--purple); color: var(--purple);
background: rgba(168,85,247,0.05); text-shadow: var(--glow-purple);
}
/* ═══════════════════════════════════════════════════════
OUTPUT PANEL (RIGHT)
═══════════════════════════════════════════════════════ */
#output-panel {
display: flex; flex-direction: column; width: 45%; min-width: 340px;
max-width: 55%; min-height: 0; 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; min-height: 0; overflow: hidden; position: relative;
}
/* Tab panes */
.tab-pane { display: none; height: 100%; min-height: 0; }
.tab-pane.active { display: flex; flex-direction: column; }
/* Agent tab β€” scrollable like the Deploy tab.
Override .tab-pane.active's `display:flex` so the single .deploy-section
child does not get squished by flex shrink, which was preventing the
pane from scrolling.
NOTE: `display:none` is inherited from `.tab-pane` (base); only the
`.active` state flips to `display:block`. Don't put `display:block` on
the base `#pane-agent` or both panes will render at the same time. */
#pane-agent {
padding: 16px;
overflow-y: auto;
overflow-x: hidden;
}
#pane-agent.active { display: block !important; }
#pane-agent > .deploy-section { flex: 0 0 auto; }
/* Preview tab */
#pane-preview {
align-items: stretch; justify-content: stretch;
position: relative; min-height: 0; overflow: hidden;
}
.preview-placeholder {
align-self: center; margin: auto; 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; position: absolute; inset: 0; width: 100%; height: 100%;
min-height: 0; 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;
}
/* ═══════════════════════════════════════════════════════
SEARCH TAB
═══════════════════════════════════════════════════════ */
#pane-search { padding: 12px 16px; gap: 12px; overflow-y: auto; }
.search-bar {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
#search-input {
flex: 1;
background: var(--bg-deep);
border: 1px solid var(--border);
color: var(--green);
font-family: var(--font-mono);
font-size: 12px;
padding: 6px 10px;
border-radius: var(--radius);
outline: none;
transition: border-color var(--transition);
}
#search-input:focus { border-color: var(--border-focus); }
#search-input::placeholder { color: var(--gray-dim); }
#btn-search-go {
background: transparent;
border: 1px solid var(--purple);
color: var(--purple);
font-family: var(--font-mono);
font-size: 11px;
padding: 6px 12px;
border-radius: var(--radius);
cursor: pointer;
transition: all var(--transition);
letter-spacing: 1px;
}
#btn-search-go:hover {
background: var(--purple);
color: white;
text-shadow: none;
}
.search-result-item {
padding: 8px 0;
border-bottom: 1px solid var(--border);
}
.search-result-item:last-child { border-bottom: none; }
.search-result-title {
color: var(--cyan);
font-size: 12px;
font-weight: 600;
text-decoration: none;
display: block;
margin-bottom: 2px;
}
.search-result-title:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
.search-result-url {
color: var(--green-dim);
font-size: 10px;
display: block;
margin-bottom: 2px;
word-break: break-all;
}
.search-result-snippet {
color: var(--gray-mid);
font-size: 11px;
line-height: 1.4;
}
.search-results-empty {
color: var(--gray-dim);
font-size: 12px;
text-align: center;
padding: 40px 20px;
}
/* Gradio preview */
.gradio-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: rgba(168,85,247,0.15);
border: 1px solid var(--purple);
border-radius: 10px;
font-size: 10px;
color: var(--purple);
letter-spacing: 0.5px;
}
#gradio-iframe {
display: none;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
min-height: 0;
border: none;
}
/* ═══════════════════════════════════════════════════════
DEPLOY TAB
═══════════════════════════════════════════════════════ */
#pane-deploy {
padding: 16px;
overflow-y: auto;
overflow-x: hidden;
}
#pane-deploy.active { display: block !important; }
#pane-deploy > .deploy-section { flex: 0 0 auto; }
.deploy-section {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 14px;
background: var(--bg-code);
}
.deploy-title {
font-size: 12px;
font-weight: 600;
color: var(--purple);
text-shadow: var(--glow-purple);
margin-bottom: 10px;
letter-spacing: 1px;
text-transform: uppercase;
}
.deploy-field {
margin-bottom: 10px;
}
.deploy-field label {
display: block;
font-size: 10px;
color: var(--gray-dim);
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 4px;
}
.deploy-field input, .deploy-field select {
width: 100%;
background: var(--bg-deep);
border: 1px solid var(--border);
color: var(--green);
font-family: var(--font-mono);
font-size: 12px;
padding: 6px 10px;
border-radius: var(--radius);
outline: none;
transition: border-color var(--transition);
}
.deploy-field input:focus, .deploy-field select:focus {
border-color: var(--border-focus);
}
.deploy-field input::placeholder {
color: var(--gray-dim);
}
.deploy-field select option {
background: var(--bg-deep);
color: var(--gray-light);
}
.deploy-hint {
font-size: 10px;
color: var(--gray-dim);
margin-top: 3px;
}
#btn-push-hf {
width: 100%;
background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(57,255,20,0.1));
border: 1px solid var(--purple);
color: var(--purple);
font-family: var(--font-mono);
font-size: 12px;
padding: 8px 14px;
border-radius: var(--radius);
cursor: pointer;
transition: all var(--transition);
letter-spacing: 1px;
margin-top: 6px;
}
#btn-push-hf:hover:not(:disabled) {
background: var(--purple);
color: white;
box-shadow: 0 0 12px rgba(168,85,247,0.4);
text-shadow: none;
}
#btn-push-hf:disabled {
opacity: 0.4;
cursor: not-allowed;
}
#btn-hf-login {
background: linear-gradient(135deg, #FFD21E, #FF9D00);
border: none;
color: #1a1a1a;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
transition: all var(--transition);
letter-spacing: 0.5px;
}
#btn-hf-login:hover {
box-shadow: 0 0 12px rgba(255,210,30,0.5);
transform: translateY(-1px);
}
#hf-owner {
width: 100%;
background: var(--bg-code);
color: var(--gray-light);
border: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 12px;
padding: 6px 10px;
border-radius: var(--radius);
cursor: pointer;
}
.deploy-status {
margin-top: 10px;
padding: 8px 12px;
border-radius: var(--radius);
font-size: 11px;
display: none;
}
.deploy-status.success {
display: block;
background: rgba(80,250,123,0.1);
border: 1px solid var(--success);
color: var(--success);
}
.deploy-status.error {
display: block;
background: rgba(255,85,85,0.1);
border: 1px solid var(--red);
color: var(--red);
}
.deploy-status.working {
display: block;
background: rgba(255,179,0,0.1);
border: 1px solid var(--amber);
color: var(--amber);
}
.deploy-status a {
color: var(--cyan);
font-weight: 600;
}
/* Project files list */
.project-files {
margin-top: 10px;
}
.file-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 0;
font-size: 11px;
color: var(--gray-mid);
border-bottom: 1px solid rgba(30,42,58,0.5);
}
.file-item:last-child { border-bottom: none; }
.file-icon { color: var(--amber); }
.file-name { color: var(--cyan); }
/* ═══════════════════════════════════════════════════════
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; }
#target-selector { gap: 6px; }
}
</style>
</head>
<body>
<div id="app">
<!-- Header -->
<header id="header">
<div class="header-title">
<div class="header-ascii">&#9556;&#9552;&#9552;&#9552; FULLSTACK CODE BUILDER &#9552;&#9552;&#9552;&#9562;</div>
<div class="header-subtitle">Local AI App Generator | <span id="header-model-name">MiniCPM5-1B</span></div>
</div>
<div class="header-actions">
<a class="pill" id="model-pill" href="#" target="_blank" rel="noopener">
<span class="dot loading" id="model-dot"></span>
<span id="model-pill-text">MiniCPM5-1B</span>
</a>
<select id="model-select" onchange="onModelChange()" title="Switch AI model">
<option value="minicpm5-1b">MiniCPM5-1B (text)</option>
<option value="minicpm-v-4.6">MiniCPM-V-4.6 (vision)</option>
</select>
<button id="btn-thinking" class="btn-thinking active" onclick="toggleThinking()" title="Show/hide thinking blocks">🧠 Think</button>
<button id="btn-new-chat" onclick="newChat()" title="Start a new chat session">[NEW]</button>
</div>
</header>
<div id="playground-banner">
Powered by <a id="banner-model-link" href="https://huggingface.co/openbmb/MiniCPM5-1B" target="_blank" rel="noopener"><strong>MiniCPM5-1B</strong></a> running locally &mdash; no external APIs. Generate fullstack apps in any language and deploy to HuggingFace.
</div>
<!-- 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-selector">
<div class="selector-group">
<span class="selector-label">Lang:</span>
<select id="lang-select" onchange="onLanguageChange()"></select>
</div>
<div class="selector-group">
<span class="selector-label">Framework:</span>
<select id="framework-select"></select>
</div>
<div class="selector-group" id="image-attach-group" style="display:none;">
<input type="file" id="image-upload" accept="image/*" style="display:none" onchange="onImageUpload(event)">
<button id="btn-attach-image" onclick="document.getElementById('image-upload').click()" title="Attach image (VLM only)">πŸ“·</button>
<span id="image-attach-name" style="font-size:10px;color:var(--gray-dim);max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
<button id="btn-remove-image" onclick="removeImage()" title="Remove image" style="display:none;font-size:10px;color:var(--red);background:none;border:none;cursor:pointer;">βœ•</button>
</div>
</div>
<div id="input-row">
<span class="input-prompt-symbol">&#10095;</span>
<textarea id="chat-input" rows="1" placeholder="Describe the app you want to build..." spellcheck="false"></textarea>
<button id="btn-web-search" onclick="searchAndGenerate()" title="Search web + Generate">&#128269;</button>
<button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">&#10148;</button>
<button id="btn-stop" onclick="stopGeneration()" title="Stop generation">&#9632; 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>
<button class="output-tab" data-tab="search" onclick="switchTab('search')">Search</button>
<button class="output-tab" data-tab="agent" onclick="switchTab('agent')">Agent</button>
<button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</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">
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474; &#9585;&#9473;&#9473;&#9473;&#9586; &#9474;
&#9474; &#9474; &#9654; &#9474; OUTPUT &#9474;
&#9474; &#9589;&#9473;&#9473;&#9473;&#9588; &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</div>
<div class="placeholder-text">Generate 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()">&#10570; 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">&mdash;</span>
<div class="code-tab-actions">
<button class="code-tab-btn" id="btn-copy-code" onclick="copyCode()">&#128203; Copy</button>
<a class="code-tab-btn" id="btn-download" href="#" style="display:none;">&#11015; Download</a>
</div>
</div>
<div id="code-display">
<div class="code-placeholder">No code generated yet.</div>
</div>
</div>
<!-- Search Pane -->
<div class="tab-pane" id="pane-search">
<div class="search-bar">
<input type="text" id="search-input" placeholder="Search the web... (Google, no API needed)" spellcheck="false">
<button id="btn-search-go" onclick="doWebSearch()">&#128269; Search</button>
</div>
<div id="search-results">
<div class="search-results-empty">Search the web for documentation, examples, and references to use in your code.</div>
</div>
</div>
<!-- Agent Pane (Claude Code-style) -->
<div class="tab-pane" id="pane-agent">
<div class="deploy-section">
<div class="deploy-title">&#129302; Agent Mode (Claude Code-style)</div>
<!-- Agent mode toggle -->
<div class="deploy-field">
<label>Agent Mode</label>
<div style="display:flex;align-items:center;gap:12px;">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-weight:400;">
<input type="checkbox" id="agent-mode-toggle" onchange="toggleAgentMode()" style="cursor:pointer;">
<span>Enable agent loop (model calls tools: read/write/edit/glob/grep/bash/todos)</span>
</label>
</div>
<div class="deploy-hint">When ON, the model can manipulate files in the sandboxed workspace and run shell commands.</div>
</div>
<!-- Slash Commands -->
<div class="deploy-field">
<label>Slash Commands</label>
<div id="commands-list" style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11px;"></div>
<div class="deploy-hint">Type the command in chat (e.g. <code>/commit</code>, <code>/review</code>)</div>
</div>
<!-- Skills -->
<div class="deploy-field">
<label>Skills</label>
<div id="skills-list" style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11px;"></div>
<div class="deploy-hint">Click a skill to activate it for the next prompt</div>
</div>
<!-- Active skills display -->
<div class="deploy-field" id="active-skills-section" style="display:none;">
<label>Active Skills</label>
<div id="active-skills-display" style="display:flex;flex-wrap:wrap;gap:4px;"></div>
<button onclick="clearActiveSkills()" style="margin-top:6px;font-size:10px;color:var(--red);background:none;border:1px solid var(--red);padding:2px 8px;cursor:pointer;border-radius:3px;">Clear all</button>
</div>
<!-- Custom Agents -->
<div class="deploy-field">
<label>&#129302; Custom Agents <span id="active-agent-badge" style="display:none;background:var(--purple-dim, #2d1b4e);color:var(--purple, #a855f7);padding:1px 6px;border-radius:3px;font-size:9px;margin-left:6px;border:1px solid var(--purple, #a855f7);">active: <span id="active-agent-name">-</span></span></label>
<div id="agents-list" style="display:grid;grid-template-columns:1fr;gap:4px;font-size:11px;"></div>
<div class="deploy-hint">
Click an agent to activate it. Built-in agents: <code>code-reviewer</code>, <code>test-writer</code>.
Or describe a new one in natural language and let the AI generate it:
</div>
<!-- AI agent creation box -->
<div style="margin-top:8px;border:1px dashed var(--border);padding:8px;border-radius:4px;background:var(--bg-code);">
<div style="font-size:10px;color:var(--gray-light);margin-bottom:4px;font-weight:600;">&#10024; AI-Generate a Custom Agent</div>
<textarea id="agent-create-input" rows="2" placeholder="e.g. 'a security reviewer that audits dependencies and flags hardcoded secrets'" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:6px;font-family:var(--font-mono);font-size:11px;resize:vertical;"></textarea>
<div style="display:flex;gap:6px;margin-top:6px;">
<button onclick="createAgentViaAI()" id="btn-create-agent-ai" style="font-size:10px;background:var(--green);color:#000;border:none;padding:4px 10px;cursor:pointer;border-radius:3px;font-weight:600;">&#10024; Generate via AI</button>
<button onclick="showManualAgentEditor()" style="font-size:10px;background:none;border:1px solid var(--border);padding:3px 10px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Write manually</button>
</div>
</div>
<!-- Manual editor (hidden by default) -->
<div id="manual-agent-editor" style="display:none;margin-top:8px;border:1px solid var(--border);padding:8px;border-radius:4px;background:var(--bg-code);">
<div style="font-size:10px;color:var(--gray-light);margin-bottom:4px;font-weight:600;">Manual Agent Editor</div>
<input type="text" id="agent-name-input" placeholder="name (kebab-case)" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-family:var(--font-mono);font-size:11px;margin-bottom:4px;">
<input type="text" id="agent-desc-input" placeholder="one-line description" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-size:11px;margin-bottom:4px;">
<input type="text" id="agent-tools-input" placeholder="tools (comma-sep, e.g. read_file,grep,bash)" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-family:var(--font-mono);font-size:11px;margin-bottom:4px;">
<input type="text" id="agent-skills-input" placeholder="skills (comma-sep, e.g. code-review)" style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-family:var(--font-mono);font-size:11px;margin-bottom:4px;">
<div style="display:flex;gap:4px;margin-bottom:4px;">
<input type="text" id="agent-temp-input" placeholder="temp (0.0-1.0)" style="flex:1;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-size:11px;">
<input type="text" id="agent-iter-input" placeholder="max_iter" style="flex:1;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:4px 6px;font-size:11px;">
</div>
<textarea id="agent-body-input" rows="6" placeholder="# My Agent&#10;&#10;Full system-prompt extension here..." style="width:100%;background:var(--bg-input,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:6px;font-family:var(--font-mono);font-size:11px;resize:vertical;margin-bottom:4px;"></textarea>
<div style="display:flex;gap:6px;">
<button onclick="saveManualAgent()" style="font-size:10px;background:var(--green);color:#000;border:none;padding:4px 10px;cursor:pointer;border-radius:3px;font-weight:600;">Save agent</button>
<button onclick="hideManualAgentEditor()" style="font-size:10px;background:none;border:1px solid var(--border);padding:3px 10px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Cancel</button>
</div>
</div>
</div>
<!-- Hooks -->
<div class="deploy-field">
<label>Hooks (active rules)</label>
<div id="hooks-list" style="display:grid;grid-template-columns:1fr;gap:4px;font-size:11px;"></div>
<div class="deploy-hint">Rules fire on bash/file/prompt events. Add custom rules in <code>workspace/.sonicoder/hooks/</code></div>
</div>
<!-- Todo List -->
<div class="deploy-field">
<label>Todo List</label>
<div id="todos-display" style="font-size:11px;max-height:200px;overflow-y:auto;">
<div style="color:var(--gray-dim);">No todos yet. Use the agent to create some.</div>
</div>
<button onclick="refreshTodos()" style="margin-top:6px;font-size:10px;background:none;border:1px solid var(--border);padding:2px 8px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Refresh</button>
</div>
<!-- Workspace -->
<div class="deploy-field">
<label>Workspace Files</label>
<div id="workspace-tree" style="font-size:11px;max-height:300px;overflow-y:auto;font-family:var(--font-mono);background:var(--bg-code);padding:8px;border-radius:4px;border:1px solid var(--border);">
<div style="color:var(--gray-dim);">Empty. The agent will create files here.</div>
</div>
<div style="display:flex;gap:6px;margin-top:6px;">
<button onclick="refreshWorkspace()" style="font-size:10px;background:none;border:1px solid var(--border);padding:2px 8px;cursor:pointer;border-radius:3px;color:var(--gray-light);">Refresh</button>
<button onclick="resetWorkspace()" style="font-size:10px;background:none;border:1px solid var(--red);padding:2px 8px;cursor:pointer;border-radius:3px;color:var(--red);">Reset workspace</button>
</div>
<div class="deploy-hint">Files live in <code>./workspace/</code> β€” sandboxed, path-escape protected</div>
</div>
</div>
</div>
<!-- Deploy Pane -->
<div class="tab-pane" id="pane-deploy">
<!-- ─── GitHub Import Section ──────────────────────────────── -->
<div class="deploy-section" style="margin-bottom:14px;">
<div class="deploy-title" style="color:var(--cyan, #00d4ff);text-shadow:0 0 6px rgba(0,212,255,0.4);">&#128226; Import Project from GitHub</div>
<div class="deploy-hint" style="margin-bottom:10px;">
Pull a remote repo into the current workspace. Accepts
<code>https://github.com/owner/repo</code>,
<code>/tree/branch/subdir</code> URLs, and SSH form.
The repo is shallow-cloned and copied into the workspace with
<code>.git</code>, <code>node_modules</code>, <code>__pycache__</code>,
<code>.venv</code> stripped. Or type <code>/github &lt;url&gt;</code> in chat for the AI to do it.
</div>
<div class="deploy-field" style="border-left:3px solid var(--cyan, #00d4ff);padding-left:10px;margin-bottom:0;">
<label>Repository URL</label>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px;">
<input type="text" id="github-url-input" placeholder="https://github.com/owner/repo (or owner/repo/tree/main/subdir)" style="flex:1;min-width:220px;background:var(--bg-deep,#0d1117);color:var(--text);border:1px solid var(--border);border-radius:3px;padding:6px 10px;font-family:var(--font-mono);font-size:11px;">
<button onclick="importGithub()" id="btn-import-github" style="font-size:11px;background:var(--cyan, #00d4ff);color:#000;border:none;padding:6px 14px;cursor:pointer;border-radius:3px;font-weight:600;">&#11015; Import</button>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:4px;font-size:10px;color:var(--gray-dim);">
<input type="text" id="github-branch-input" placeholder="branch (optional)" style="flex:1;min-width:120px;max-width:200px;background:var(--bg-deep,#0d1117);color:var(--green);border:1px solid var(--border);border-radius:3px;padding:4px 8px;font-family:var(--font-mono);font-size:10px;">
<input type="text" id="github-subdir-input" placeholder="subdir (optional)" style="flex:1;min-width:120px;max-width:200px;background:var(--bg-deep,#0d1117);color:var(--green);border:1px solid var(--border);border-radius:3px;padding:4px 8px;font-family:var(--font-mono);font-size:10px;">
<input type="text" id="github-into-input" placeholder="into path (optional)" style="flex:1;min-width:120px;max-width:200px;background:var(--bg-deep,#0d1117);color:var(--green);border:1px solid var(--border);border-radius:3px;padding:4px 8px;font-family:var(--font-mono);font-size:10px;">
</div>
<div id="github-import-status" style="font-size:10px;margin-top:4px;color:var(--gray-dim);"></div>
</div>
</div>
<!-- ─── HuggingFace Deploy Section ─────────────────────────── -->
<div class="deploy-section">
<div class="deploy-title">&#128640; Deploy to HuggingFace</div>
<!-- OAuth Login Section -->
<div class="deploy-field" id="hf-auth-section">
<label>Sign In</label>
<div id="hf-auth-container">
<button id="btn-hf-login" onclick="loginWithHF()">&#129309; Sign in with HuggingFace</button>
<div id="hf-user-info" style="display:none;">
<img id="hf-user-avatar" src="" alt="" style="width:24px;height:24px;border-radius:50%;vertical-align:middle;margin-right:6px;">
<span id="hf-user-name" style="color:var(--green);font-weight:600;"></span>
<button id="btn-hf-logout" onclick="logoutHF()" style="margin-left:8px;font-size:10px;color:var(--red);background:none;border:none;cursor:pointer;">Sign out</button>
</div>
</div>
<div class="deploy-hint" id="hf-auth-hint">Sign in with OAuth β€” no token paste needed</div>
</div>
<!-- Push to: User or Org selector -->
<div class="deploy-field">
<label for="hf-owner">Push to</label>
<select id="hf-owner">
<option value="">Sign in to see options</option>
</select>
<div class="deploy-hint">Select your account or an organization</div>
</div>
<div class="deploy-field">
<label for="hf-repo-name">Repository Name</label>
<input type="text" id="hf-repo-name" placeholder="my-app" autocomplete="off">
<div class="deploy-hint">The repo will be created at owner/repo-name</div>
</div>
<div class="deploy-field" id="hf-manual-token-section">
<label for="hf-token">HuggingFace Token <span style="font-weight:400;color:var(--gray-dim);">(manual fallback)</span></label>
<input type="password" id="hf-token" placeholder="hf_xxxxxxxxxxxxxxxxxxxxx" autocomplete="off">
<div class="deploy-hint">Only needed if OAuth is unavailable. <a href="https://huggingface.co/settings/tokens" target="_blank">Get token</a></div>
</div>
<div class="deploy-field">
<label for="hf-space-sdk">Space SDK</label>
<select id="hf-space-sdk">
<option value="auto">Auto-detect</option>
<option value="docker">Docker (React/Next/Vue/Express/Node)</option>
<option value="static">Static (HTML/CSS/JS)</option>
<option value="gradio">Gradio (Python)</option>
<option value="streamlit">Streamlit (Python)</option>
</select>
<div class="deploy-hint">JS frameworks auto-use Docker with Dockerfile build</div>
</div>
<button id="btn-push-hf" onclick="pushToHuggingFace()" disabled>&#128640; Push to HuggingFace</button>
<div class="deploy-status" id="deploy-status"></div>
<div class="project-files" id="project-files"></div>
</div>
<!-- ─── GitHub Push Section ────────────────────────────────── -->
<div class="deploy-section" style="margin-top:14px;">
<div class="deploy-title" style="color:var(--cyan, #00d4ff);text-shadow:0 0 6px rgba(0,212,255,0.4);">&#128230; Push Update to GitHub</div>
<div class="deploy-hint" style="margin-bottom:10px;">
Snapshots the current workspace and pushes it as a commit to a
GitHub repo. Uses <code>--force-with-lease</code> so the latest
workspace contents overwrite the remote tip.
</div>
<!-- Only 3 required inputs -->
<div class="deploy-field">
<label for="gh-repo-name">1. Repository Name</label>
<input type="text" id="gh-repo-name" placeholder="my-app (or username/my-app)" autocomplete="off">
<div class="deploy-hint">If you omit the owner prefix, your username is used.</div>
</div>
<div class="deploy-field">
<label for="gh-token">2. GitHub API Token</label>
<input type="password" id="gh-token" placeholder="ghp_xxxxxxxxxxxxxxxxxxxxx" autocomplete="off">
<div class="deploy-hint">
Needs <code>repo</code> scope.
<a href="https://github.com/settings/tokens/new?scopes=repo&description=SoniCoder" target="_blank" rel="noopener">Create a token &nearr;</a>
</div>
</div>
<div class="deploy-field">
<label for="gh-username">3. Username (for push)</label>
<input type="text" id="gh-username" placeholder="your-github-username" autocomplete="off">
<div class="deploy-hint">The GitHub user (or org) that owns the repo and matches the token.</div>
</div>
<!-- Optional advanced settings (collapsed) -->
<details style="margin-top:8px;">
<summary style="font-size:10px;color:var(--gray-dim);cursor:pointer;letter-spacing:1px;text-transform:uppercase;">Advanced (optional)</summary>
<div class="deploy-field" style="margin-top:8px;">
<label for="gh-branch">Branch</label>
<input type="text" id="gh-branch" placeholder="main" value="main" autocomplete="off">
</div>
<div class="deploy-field">
<label for="gh-commit-msg">Commit message</label>
<input type="text" id="gh-commit-msg" placeholder="Update from SoniCoder" autocomplete="off">
<div class="deploy-hint">If empty, a timestamped message is used.</div>
</div>
</details>
<button id="btn-push-github" onclick="pushToGithub()" style="margin-top:10px;">&#128230; Push to GitHub</button>
<div class="deploy-status" id="github-deploy-status"></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">&#9679;</span>
<span id="status-text">LOADING MODEL...</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()">[&#10005; 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',
targetFramework: 'Flask',
isGenerating: false,
currentEventSource: null,
activeTab: 'preview',
lastExecution: null,
lastCode: '',
lastCodeLang: '',
pendingWebPreviewCode: '',
loadedWebPreviewCode: '',
scheduledWebPreviewCode: '',
reasoningExpanded: false,
lastReasoningPressAt: 0,
modelReady: false,
searchEnabled: false,
lastSearchResults: [],
currentSearchResults: [],
searchPanelExpanded: false,
showThinking: true,
currentModelKey: 'minicpm5-1b',
currentModelType: 'text',
uploadedImageFileUrl: '',
uploadedImageName: '',
// Agent mode (Claude Code-style)
agentMode: false,
activeSkills: [],
todos: [],
// Custom agents
activeAgent: null,
agentsList: [],
};
// ═══════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════
document.addEventListener('DOMContentLoaded', () => {
document.title = CONFIG.app_title || 'SoniCoder';
try {
if (CONFIG.model_url) {
document.getElementById('model-pill').href = CONFIG.model_url;
document.getElementById('banner-model-link').href = CONFIG.model_url;
}
const modelId = typeof CONFIG.model_id === 'string' ? CONFIG.model_id : (CONFIG.model_configs ? Object.values(CONFIG.model_configs)[0]?.name || 'AI Model' : 'AI Model');
document.getElementById('model-pill-text').textContent = modelId.split('/').pop();
} catch (e) { console.warn('Model pill setup error:', e); }
// Populate language/framework selects
populateLanguageSelects();
// Render examples
renderExamples();
// Welcome message
addSystemMessage('Welcome to SoniCoder β€” a Claude Code-style agent running locally. The model is loading (no API keys needed). Open the "Agent" tab to enable tool use (read/write/edit/glob/grep/bash), browse skills and slash commands, or just describe the app you want to build.');
// 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();
}
});
// Search input Enter key
document.getElementById('search-input')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); doWebSearch(); }
});
document.addEventListener('pointerdown', handleReasoningPress, true);
document.addEventListener('mousedown', handleReasoningPress, true);
document.addEventListener('keydown', handleReasoningKeydown, true);
document.addEventListener('keydown', handleFullscreenKeydown);
observePreviewSize();
// Poll model status
pollModelStatus();
// Check for HuggingFace OAuth session
checkGradioOAuth();
});
function autoResize() {
const el = document.getElementById('chat-input');
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
// ═══════════════════════════════════════════════════════
// MODEL STATUS POLLING
// ═══════════════════════════════════════════════════════
async function pollModelStatus() {
try {
const resp = await fetch('/api/model-status');
const data = await resp.json();
const dot = document.getElementById('model-dot');
const statusText = document.getElementById('status-text');
const indicator = document.getElementById('status-indicator');
if (data.status === 'ready') {
state.modelReady = true;
dot.className = 'dot';
statusText.textContent = 'MODEL READY';
indicator.className = 'status-indicator status-success';
document.getElementById('btn-push-hf').disabled = false;
// Update model info from server response
if (data.model_key) state.currentModelKey = data.model_key;
if (data.model_type) state.currentModelType = data.model_type;
if (data.model_name) {
document.getElementById('model-pill-text').textContent = data.model_name;
document.getElementById('header-model-name').textContent = data.model_name;
}
// Show/hide image upload based on model type
const imageGroup = document.getElementById('image-attach-group');
if (state.currentModelType === 'vlm') {
imageGroup.style.display = 'flex';
} else {
imageGroup.style.display = 'none';
}
// Sync model selector
const modelSelect = document.getElementById('model-select');
if (modelSelect && data.model_key) modelSelect.value = data.model_key;
setTimeout(() => {
if (!state.isGenerating) {
indicator.className = 'status-indicator status-idle';
statusText.textContent = 'IDLE';
}
}, 3000);
return;
} else if (data.status === 'loading') {
dot.className = 'dot loading';
statusText.textContent = 'LOADING MODEL...';
indicator.className = 'status-indicator status-working';
} else {
dot.className = 'dot error';
statusText.textContent = 'MODEL ERROR';
indicator.className = 'status-indicator status-error';
}
setTimeout(pollModelStatus, 3000);
} catch (err) {
console.error('Model status poll error:', err);
setTimeout(pollModelStatus, 5000);
}
}
// ═══════════════════════════════════════════════════════
// LANGUAGE / FRAMEWORK SELECTS
// ═══════════════════════════════════════════════════════
function populateLanguageSelects() {
const langSelect = document.getElementById('lang-select');
const fwSelect = document.getElementById('framework-select');
if (!langSelect || !fwSelect) return;
// Fallback languages if CONFIG not loaded
const languages = CONFIG.languages || [
["Python", ["Gradio", "Flask", "Django", "FastAPI", "Streamlit", "Plain Python"]],
["JavaScript", ["React", "Vue.js", "Next.js", "Express.js", "Node.js", "Vanilla JS"]],
["TypeScript", ["React", "Next.js", "Express.js", "NestJS"]],
["HTML/CSS/JS", ["Tailwind CSS", "Bootstrap", "Vanilla"]],
["Java", ["Spring Boot", "Maven", "Gradle"]],
["Go", ["Gin", "Fiber", "Echo", "Plain Go"]],
["Rust", ["Actix", "Axum", "Rocket"]],
["PHP", ["Laravel", "Symfony", "Plain PHP"]],
["Ruby", ["Rails", "Sinatra"]],
["C#", ["ASP.NET", "Blazor"]],
["Swift", ["Vapor", "SwiftUI"]],
["Kotlin", ["Ktor", "Spring Boot"]],
];
languages.forEach((entry) => {
const lang = Array.isArray(entry) ? entry[0] : entry;
const opt = document.createElement('option');
opt.value = lang;
opt.textContent = lang;
if (lang === 'Python') opt.selected = true;
langSelect.appendChild(opt);
});
onLanguageChange();
}
function onLanguageChange() {
const langSelect = document.getElementById('lang-select');
const fwSelect = document.getElementById('framework-select');
const selectedLang = langSelect.value;
state.targetLanguage = selectedLang;
// Update frameworks
fwSelect.innerHTML = '';
const languages = CONFIG.languages || [];
const entry = languages.find((e) => (Array.isArray(e) ? e[0] : e) === selectedLang);
if (entry && Array.isArray(entry) && entry[1]) {
entry[1].forEach((fw) => {
const opt = document.createElement('option');
opt.value = fw;
opt.textContent = fw;
fwSelect.appendChild(opt);
});
state.targetFramework = entry[1][0];
}
fwSelect.onchange = () => {
state.targetFramework = fwSelect.value;
autoSelectSDK(selectedLang, fwSelect.value);
};
// Auto-select SDK based on language/framework
autoSelectSDK(selectedLang, fwSelect.value);
}
function autoSelectSDK(lang, framework) {
const sdkSelect = document.getElementById('hf-space-sdk');
if (!sdkSelect) return;
const jsLangs = ['JavaScript', 'TypeScript'];
const jsFrameworks = ['React', 'Next.js', 'Vue.js', 'Express.js', 'Node.js', 'NestJS'];
const pythonFrameworks = ['Gradio', 'Streamlit', 'Flask', 'Django', 'FastAPI'];
if (jsLangs.includes(lang) || jsFrameworks.includes(framework)) {
// JS frameworks need Docker
if (jsFrameworks.includes(framework) || jsLangs.includes(lang)) {
sdkSelect.value = 'docker';
}
} else if (framework === 'Gradio') {
sdkSelect.value = 'gradio';
} else if (framework === 'Streamlit') {
sdkSelect.value = 'streamlit';
} else if (lang === 'Python' && pythonFrameworks.includes(framework)) {
sdkSelect.value = 'gradio';
} else if (lang === 'HTML/CSS/JS' || framework === 'Vanilla' || framework === 'Tailwind CSS' || framework === 'Bootstrap') {
sdkSelect.value = 'static';
} else {
sdkSelect.value = 'auto';
}
}
// ═══════════════════════════════════════════════════════
// 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 (state.isGenerating) return;
resetConversation();
if (ex.language) {
document.getElementById('lang-select').value = ex.language;
onLanguageChange();
}
if (ex.framework) {
document.getElementById('framework-select').value = ex.framework;
state.targetFramework = ex.framework;
}
sendMessage(ex.prompt);
});
row.appendChild(chip);
});
}
// ═══════════════════════════════════════════════════════
// 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">ai&gt;</span><div class="msg-body"><div class="search-source-container"></div><span class="msg-content streaming-cursor"></span></div>`;
container.appendChild(div);
state.reasoningExpanded = false;
state.currentSearchResults = [];
state.searchPanelExpanded = false;
scrollToBottom();
return div;
}
function updateAssistantMessage(content, isStreaming) {
const div = document.getElementById('current-assistant-msg');
if (!div) return;
const contentEl = div.querySelector('.msg-content');
const keepReasoningExpanded = state.reasoningExpanded || Boolean(contentEl.querySelector('.think-block.open'));
state.reasoningExpanded = keepReasoningExpanded;
contentEl.innerHTML = parseMarkdown(content);
contentEl.querySelectorAll('.think-block').forEach((block) => {
setReasoningBlockOpen(block, keepReasoningExpanded);
});
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 '';
const thinkBlocks = [];
text = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => {
const idx = thinkBlocks.length;
thinkBlocks.push(renderThinkBlock(content, '\ud83d\udcad Reasoning (click to expand)'));
return `@@THINKBLOCK_${idx}@@`;
});
text = text.replace(/<think>([\s\S]*)$/g, (_, content) => {
const idx = thinkBlocks.length;
thinkBlocks.push(renderThinkBlock(content, '\ud83d\udcad Reasoning (thinking...)'));
return `@@THINKBLOCK_${idx}@@`;
});
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 `@@CODEBLOCK_${idx}@@`;
});
text = escapeHtml(text);
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(/@@CODEBLOCK_(\d+)@@/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}')">&#128203; Copy</button></div><pre><code id="${id}">${escapedCode}</code></pre></div>`;
});
text = text.replace(/@@THINKBLOCK_(\d+)@@/g, (_, idx) => thinkBlocks[parseInt(idx)]);
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]))/g, '$1');
text = text.replace(/(<\/(?:div|ul|ol|h[1-3])>)<\/p>/g, '$1');
return text;
}
function renderThinkBlock(content, summary) {
const escapedContent = escapeHtml(content.trim()).replace(/\n/g, '<br>');
const openClass = state.reasoningExpanded ? ' open' : '';
const expanded = state.reasoningExpanded ? 'true' : 'false';
return `<div class="think-block${openClass}"><button type="button" class="think-summary" aria-expanded="${expanded}">${summary}</button><div class="think-content">${escapedContent}</div></div>`;
}
function handleReasoningPress(event) { updateReasoningFromEvent(event); }
function handleReasoningKeydown(event) {
if (event.key !== 'Enter' && event.key !== ' ') return;
updateReasoningFromEvent(event);
}
function updateReasoningFromEvent(event) {
if (event.type === 'mousedown' && Date.now() - state.lastReasoningPressAt < 500) return;
const target = event.target;
if (!target || !target.closest) return;
const button = target.closest('.think-summary');
if (!button) return;
const block = button.closest('.think-block');
if (!block) return;
event.preventDefault();
event.stopPropagation();
if (event.stopImmediatePropagation) event.stopImmediatePropagation();
state.lastReasoningPressAt = Date.now();
const nextOpen = !block.classList.contains('open');
state.reasoningExpanded = nextOpen;
const scope = block.closest('.msg-content') || document;
scope.querySelectorAll('.think-block').forEach((trace) => {
setReasoningBlockOpen(trace, nextOpen);
});
}
function setReasoningBlockOpen(block, open) {
block.classList.toggle('open', open);
const button = block.querySelector('.think-summary');
if (button) button.setAttribute('aria-expanded', open ? 'true' : 'false');
}
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 = '\u2713 Copied!';
button.classList.add('copied');
setTimeout(() => { button.textContent = '\ud83d\udccb 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 = '\u2713 Copied!';
setTimeout(() => { btn.textContent = '\ud83d\udccb 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 = '\u25d0'; break;
case 'success': indicator.classList.add('status-success'); dotEl.textContent = '\u2713'; break;
case 'error': indicator.classList.add('status-error'); dotEl.textContent = '\u2717'; break;
case 'info': indicator.classList.add('status-info'); dotEl.textContent = '\u2139'; break;
default: indicator.classList.add('status-idle'); dotEl.textContent = '\u25cf';
}
textEl.textContent = text || 'IDLE';
}
// ═══════════════════════════════════════════════════════
// OUTPUT PANEL
// ═══════════════════════════════════════════════════════
function switchTab(tab, { forcePreviewReload = false } = {}) {
const wasPreview = state.activeTab === 'preview';
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}`);
});
if (tab === 'preview') {
ensureWebPreviewLoaded({ forceReload: forcePreviewReload || !wasPreview });
}
}
function renderExecution(execution) {
if (!execution) return;
state.lastExecution = execution;
// Console
document.getElementById('console-stdout').textContent = execution.stdout || 'No output.';
document.getElementById('console-stderr').textContent = execution.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 = getPreviewIframe();
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' && state.activeTab !== 'search' && state.activeTab !== 'deploy') {
switchTab('preview');
}
} else if (execution.is_web && execution.code) {
placeholder.style.display = 'none';
img.style.display = 'none';
iframe.style.display = 'block';
fsBtn.style.display = 'block';
state.pendingWebPreviewCode = execution.code;
state.loadedWebPreviewCode = '';
state.scheduledWebPreviewCode = '';
if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'search' && state.activeTab !== 'deploy') {
switchTab('preview', { forcePreviewReload: true });
} else {
iframe.srcdoc = '';
}
} else if (execution.is_gradio && execution.gradio_url) {
// Gradio app handling
placeholder.style.display = 'none';
img.style.display = 'none';
iframe.style.display = 'block';
fsBtn.style.display = 'block';
// Show Gradio badge
const badge = document.createElement('span');
badge.className = 'gradio-badge';
badge.innerHTML = '\u26a1 Gradio';
const codeTabHeader = document.querySelector('.code-tab-header');
if (codeTabHeader) codeTabHeader.prepend(badge);
state.pendingWebPreviewCode = `<html><body style="margin:0;display:flex;align-items:center;justify-content:center;height:100vh;font-family:monospace;background:#0d1117;color:#a855f7;"><div style="text-align:center"><h2>\u26a1 Gradio App Running</h2><p>App is running at: <a href="${execution.gradio_url}" target="_blank" style="color:#00d4ff">${execution.gradio_url}</a></p><p style="color:#8b949e;font-size:12px">Open in a new tab to interact with the Gradio interface</p></div></body></html>`;
state.loadedWebPreviewCode = '';
state.scheduledWebPreviewCode = '';
switchTab('preview', { forcePreviewReload: true });
} else {
if (execution.stdout || execution.stderr) {
const suggested = execution.suggested_tab || 'console';
if (state.activeTab !== 'deploy') switchTab(suggested);
}
}
// Deploy tab - project files
renderProjectFiles(execution.project_files || {});
// Enable deploy button
document.getElementById('btn-push-hf').disabled = !execution.code;
}
function renderProjectFiles(files) {
const container = document.getElementById('project-files');
if (!files || Object.keys(files).length === 0) {
container.innerHTML = '';
return;
}
let html = '<div style="margin-top: 12px; font-size: 10px; color: var(--gray-dim); letter-spacing: 1px; text-transform: uppercase;">Project Files:</div>';
for (const [filepath, content] of Object.entries(files)) {
const ext = filepath.split('.').pop();
const icon = getFileIcon(ext);
html += `<div class="file-item"><span class="file-icon">${icon}</span><span class="file-name">${escapeHtml(filepath)}</span><span style="color:var(--gray-dim);font-size:10px;">(${content.length} chars)</span></div>`;
}
container.innerHTML = html;
}
function getFileIcon(ext) {
const icons = {
'py': '\ud83d\udc0d', 'js': '\u26a1', 'ts': '\ud83d\udde1\ufe0f', 'html': '\ud83c\udf10',
'css': '\ud83c\udfa8', 'json': '\ud83d\udcc4', 'md': '\ud83d\udcd3', 'yml': '\u2699\ufe0f',
'yaml': '\u2699\ufe0f', 'java': '\u2615', 'go': '\ud83e\udd85', 'rs': '\ud83e\udd80',
'php': '\ud83d\udc18', 'rb': '\ud83d\udc8e', 'swift': '\ud83e\udd85', 'kt': '\ud83c\udf0a',
};
return icons[ext] || '\ud83d\udcc1';
}
function resetOutput() {
const iframe = getPreviewIframe();
document.getElementById('preview-placeholder').style.display = '';
document.getElementById('preview-image').style.display = 'none';
iframe.style.display = 'none';
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 = '\u2014';
document.getElementById('btn-download').style.display = 'none';
document.getElementById('project-files').innerHTML = '';
document.getElementById('deploy-status').className = 'deploy-status';
document.getElementById('deploy-status').style.display = 'none';
state.lastExecution = null;
state.lastCode = '';
state.lastCodeLang = '';
state.pendingWebPreviewCode = '';
state.loadedWebPreviewCode = '';
state.scheduledWebPreviewCode = '';
}
// ═══════════════════════════════════════════════════════
// FULLSCREEN
// ═══════════════════════════════════════════════════════
function getPreviewIframe() { return document.getElementById('preview-iframe'); }
function recreatePreviewIframe() {
const oldFrame = getPreviewIframe();
const freshFrame = document.createElement('iframe');
freshFrame.id = 'preview-iframe';
freshFrame.setAttribute('sandbox', 'allow-scripts');
freshFrame.style.display = oldFrame.style.display || 'block';
oldFrame.replaceWith(freshFrame);
return freshFrame;
}
function ensureWebPreviewLoaded({ forceReload = false } = {}) {
const iframe = getPreviewIframe();
if (!state.pendingWebPreviewCode || state.activeTab !== 'preview' || iframe.style.display === 'none') return;
if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) {
schedulePreviewResize(iframe);
return;
}
if (!forceReload && state.scheduledWebPreviewCode === state.pendingWebPreviewCode) return;
state.scheduledWebPreviewCode = state.pendingWebPreviewCode;
iframe.srcdoc = '';
const loadWhenLaidOut = () => {
if (state.activeTab !== 'preview' || !state.pendingWebPreviewCode) {
state.scheduledWebPreviewCode = '';
return;
}
if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) return;
const visibleFrame = getPreviewIframe();
const rect = visibleFrame.getBoundingClientRect();
if (rect.width < 10 || rect.height < 10) {
state.scheduledWebPreviewCode = '';
setTimeout(() => ensureWebPreviewLoaded({ forceReload }), 50);
return;
}
const freshFrame = recreatePreviewIframe();
freshFrame.srcdoc = state.pendingWebPreviewCode;
state.loadedWebPreviewCode = state.pendingWebPreviewCode;
state.scheduledWebPreviewCode = '';
schedulePreviewResize(freshFrame);
};
requestAnimationFrame(() => requestAnimationFrame(loadWhenLaidOut));
setTimeout(loadWhenLaidOut, 75);
}
function schedulePreviewResize(iframe) {
const dispatchResize = () => {
try { iframe.contentWindow?.dispatchEvent(new Event('resize')); } catch (_err) {}
};
requestAnimationFrame(() => requestAnimationFrame(dispatchResize));
setTimeout(dispatchResize, 100);
setTimeout(dispatchResize, 350);
}
function observePreviewSize() {
const previewPane = document.getElementById('pane-preview');
if (!previewPane) return;
window.addEventListener('resize', () => {
if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
schedulePreviewResize(getPreviewIframe());
}
});
if (typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver(() => {
if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
schedulePreviewResize(getPreviewIframe());
}
});
observer.observe(previewPane);
}
function openFullscreen() {
const overlay = document.getElementById('fullscreen-overlay');
const iframe = document.getElementById('fullscreen-iframe');
if (state.lastExecution && state.lastExecution.is_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 handleFullscreenKeydown(event) {
if (event.key !== 'Escape') return;
const overlay = document.getElementById('fullscreen-overlay');
if (!overlay.classList.contains('active')) return;
event.preventDefault();
closeFullscreen();
}
// ═══════════════════════════════════════════════════════
// 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;
if (!state.modelReady) {
addSystemMessage('The model is still loading. Please wait...');
return;
}
state.isGenerating = true;
toggleInputState(true);
addUserMessage(prompt);
addAssistantMessage();
renderStatus('Thinking...', 'working');
const historyJSON = JSON.stringify(state.history);
const execContextJSON = JSON.stringify(state.executionContext);
const framework = document.getElementById('framework-select')?.value || state.targetFramework;
try {
const resp = await fetch('/gradio_api/call/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [prompt, state.targetLanguage, framework, historyJSON, execContextJSON, state.searchEnabled ? 'true' : 'false', state.uploadedImageFileUrl || '']
})
});
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;
eventSource.addEventListener('generating', (e) => {
try {
const dataArray = JSON.parse(e.data);
const payload = JSON.parse(dataArray[0]);
handlePayload(payload, true);
} 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');
// Handle search results (show inline source badge)
if (payload.type === 'search_results' && payload.search_results) {
state.currentSearchResults = payload.search_results;
state.lastSearchResults = payload.search_results;
renderSearchSourceBadge(payload.search_results, false);
// Also render in the Search tab
renderSearchResults(payload.search_results);
}
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 = 'Describe the app you want 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();
}
// ═══════════════════════════════════════════════════════
// AGENT MODE (Claude Code-style)
// ═══════════════════════════════════════════════════════
async function callAgentApi(name, data) {
// Call a Gradio API endpoint and return an event source
const resp = await fetch(`/gradio_api/call/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: data }),
});
if (!resp.ok) throw new Error(`API ${name} failed: ${resp.status}`);
const { event_id } = await resp.json();
return new EventSource(`/gradio_api/call/${name}/${event_id}`);
}
function toggleAgentMode() {
state.agentMode = document.getElementById('agent-mode-toggle').checked;
if (state.agentMode) {
addSystemMessage('πŸ€– Agent mode enabled. The model can now call tools (read_file, write_file, edit_file, bash, etc.) and manipulate the workspace. Slash commands like /commit, /review, /feature, /agent are also active.');
// Refresh workspace + todos + skills + agents display
refreshWorkspace();
refreshTodos();
refreshAgents();
} else {
addSystemMessage('Agent mode disabled. Back to standard chat mode.');
}
}
function renderSkillsList() {
const container = document.getElementById('skills-list');
if (!container) return;
const skills = (CONFIG.skills || []);
if (skills.length === 0) {
container.innerHTML = '<div style="color:var(--gray-dim);grid-column:1/-1;">No skills available.</div>';
return;
}
container.innerHTML = skills.map(s => {
const isActive = state.activeSkills.includes(s.name);
const bg = isActive ? 'var(--green-dim)' : 'var(--bg-code)';
const color = isActive ? 'var(--green)' : 'var(--gray-light)';
const border = isActive ? 'var(--green)' : 'var(--border)';
return `<div style="padding:6px 8px;background:${bg};color:${color};border:1px solid ${border};border-radius:3px;cursor:pointer;" onclick="toggleSkill('${s.name}')">
<div style="font-weight:600;">${s.name}</div>
<div style="font-size:10px;color:var(--gray-mid);margin-top:2px;">${(s.description || '').slice(0, 80)}</div>
</div>`;
}).join('');
}
function toggleSkill(name) {
const idx = state.activeSkills.indexOf(name);
if (idx >= 0) {
state.activeSkills.splice(idx, 1);
} else {
state.activeSkills.push(name);
}
renderSkillsList();
renderActiveSkills();
}
function renderActiveSkills() {
const section = document.getElementById('active-skills-section');
const display = document.getElementById('active-skills-display');
if (!section || !display) return;
if (state.activeSkills.length === 0) {
section.style.display = 'none';
return;
}
section.style.display = 'block';
display.innerHTML = state.activeSkills.map(name =>
`<span style="background:var(--green-dim);color:var(--green);padding:2px 8px;border:1px solid var(--green);border-radius:3px;font-size:10px;">${name} <span style="cursor:pointer;margin-left:4px;" onclick="toggleSkill('${name}')">x</span></span>`
).join('');
}
function clearActiveSkills() {
state.activeSkills = [];
renderSkillsList();
renderActiveSkills();
}
function renderCommandsList() {
const container = document.getElementById('commands-list');
if (!container) return;
const commands = (CONFIG.commands || []);
if (commands.length === 0) {
container.innerHTML = '<div style="color:var(--gray-dim);grid-column:1/-1;">No commands available.</div>';
return;
}
container.innerHTML = commands.map(c =>
`<div style="padding:6px 8px;background:var(--bg-code);border:1px solid var(--border);border-radius:3px;">
<div style="font-weight:600;color:var(--cyan);">/${c.name}</div>
<div style="font-size:10px;color:var(--gray-mid);margin-top:2px;">${(c.description || '').slice(0, 80)}</div>
</div>`
).join('');
}
function renderHooksList() {
const container = document.getElementById('hooks-list');
if (!container) return;
const hooks = (CONFIG.hooks || []);
if (hooks.length === 0) {
container.innerHTML = '<div style="color:var(--gray-dim);">No hooks configured.</div>';
return;
}
container.innerHTML = hooks.map(h => {
const enabledColor = h.enabled ? 'var(--green)' : 'var(--gray-dim)';
const actionColor = h.action === 'block' ? 'var(--red)' : 'var(--amber)';
return `<div style="padding:6px 8px;background:var(--bg-code);border:1px solid var(--border);border-radius:3px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;color:${enabledColor};">${h.enabled ? 'ON' : 'OFF'} ${h.name}</span>
<span style="font-size:10px;color:${actionColor};">[${h.action}] ${h.event}</span>
</div>
<div style="font-size:10px;color:var(--gray-mid);margin-top:2px;font-family:var(--font-mono);">${(h.pattern || '').slice(0, 80)}</div>
</div>`;
}).join('');
}
// ═══════════════════════════════════════════════════════
// CUSTOM AGENTS (AI-generated personas)
// ═══════════════════════════════════════════════════════
function renderAgentsList() {
const container = document.getElementById('agents-list');
if (!container) return;
const agents = (state.agentsList || CONFIG.agents || []);
if (agents.length === 0) {
container.innerHTML = '<div style="color:var(--gray-dim);">No agents yet. Describe one above to have the AI generate it, or use <code>/agent create</code> in chat.</div>';
return;
}
container.innerHTML = agents.map(a => {
const isActive = state.activeAgent === a.name;
const bg = isActive ? 'var(--purple-dim, #2d1b4e)' : 'var(--bg-code)';
const color = isActive ? 'var(--purple, #a855f7)' : 'var(--gray-light)';
const border = isActive ? 'var(--purple, #a855f7)' : 'var(--border)';
const tools = (a.tools || []).slice(0, 4).join(', ') + ((a.tools || []).length > 4 ? '...' : '');
const isBuiltin = (a.author === 'builtin');
const delBtn = isBuiltin ? '' :
`<span style="cursor:pointer;color:var(--red);margin-left:6px;font-size:10px;" onclick="event.stopPropagation();deleteAgent('${a.name}')" title="Delete">x</span>`;
return `<div style="padding:6px 8px;background:${bg};color:${color};border:1px solid ${border};border-radius:3px;cursor:pointer;" onclick="activateAgent('${a.name}')">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;">${isActive ? 'β˜… ' : ''}${a.name} <span style="font-size:9px;color:var(--gray-mid);font-weight:400;">[${a.author || 'user'}]</span></span>
${delBtn}
</div>
<div style="font-size:10px;color:var(--gray-mid);margin-top:2px;">${(a.description || '').slice(0, 100)}</div>
${tools ? `<div style="font-size:9px;color:var(--gray-dim);margin-top:2px;font-family:var(--font-mono);">tools: ${tools}</div>` : ''}
</div>`;
}).join('');
}
function renderActiveAgentBadge() {
const badge = document.getElementById('active-agent-badge');
const name = document.getElementById('active-agent-name');
if (!badge || !name) return;
if (state.activeAgent) {
badge.style.display = 'inline';
name.textContent = state.activeAgent;
} else {
badge.style.display = 'none';
name.textContent = '-';
}
}
async function refreshAgents() {
try {
const es = await callAgentApi('list_agents', []);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
if (result.success) {
state.agentsList = result.agents || [];
state.activeAgent = result.active_agent || null;
renderAgentsList();
renderActiveAgentBadge();
}
} catch (err) { console.error('Agent refresh error:', err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('refreshAgents failed:', err); }
}
async function activateAgent(name) {
try {
const es = await callAgentApi('set_active_agent', [name]);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
if (result.success) {
state.activeAgent = result.active_agent || name;
state.agentsList = result.agents || state.agentsList;
renderAgentsList();
renderActiveAgentBadge();
addSystemMessage(`β˜… Active agent: ${state.activeAgent}. Subsequent prompts will use this persona and tool whitelist. Use /agent reset to revert.`);
} else {
addSystemMessage(`Failed to activate agent: ${result.error || 'unknown'}`);
}
} catch (err) { console.error('Agent activate error:', err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('activateAgent failed:', err); }
}
async function resetAgent() {
try {
const es = await callAgentApi('set_active_agent', ['']);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
if (result.success) {
state.activeAgent = null;
renderAgentsList();
renderActiveAgentBadge();
addSystemMessage('Active agent reset to default SoniCoder.');
}
} catch (err) { console.error('Agent reset error:', err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('resetAgent failed:', err); }
}
async function deleteAgent(name) {
if (!confirm(`Delete agent '${name}'? This cannot be undone.`)) return;
try {
const es = await callAgentApi('delete_agent', [name]);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
if (result.success) {
addSystemMessage(`Agent '${name}' deleted.`);
refreshAgents();
} else {
addSystemMessage(`Failed to delete agent: ${result.error || 'unknown'}`);
}
} catch (err) { console.error('Agent delete error:', err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('deleteAgent failed:', err); }
}
function showManualAgentEditor() {
document.getElementById('manual-agent-editor').style.display = 'block';
}
function hideManualAgentEditor() {
document.getElementById('manual-agent-editor').style.display = 'none';
}
async function saveManualAgent() {
const name = document.getElementById('agent-name-input').value.trim();
const desc = document.getElementById('agent-desc-input').value.trim();
const body = document.getElementById('agent-body-input').value.trim();
const tools = document.getElementById('agent-tools-input').value.trim();
const skills = document.getElementById('agent-skills-input').value.trim();
const temp = document.getElementById('agent-temp-input').value.trim();
const iter = document.getElementById('agent-iter-input').value.trim();
if (!name || !body) {
addSystemMessage('Name and body are required to save an agent.');
return;
}
try {
const es = await callAgentApi('save_agent', [name, desc, body, tools, skills, temp, iter, '', 'user']);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
if (result.success) {
addSystemMessage(`Agent '${name}' saved. Click it in the list (or /agent use ${name}) to activate.`);
hideManualAgentEditor();
['agent-name-input','agent-desc-input','agent-body-input','agent-tools-input','agent-skills-input','agent-temp-input','agent-iter-input']
.forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
refreshAgents();
} else {
addSystemMessage(`Failed to save agent: ${result.error || 'unknown'}`);
}
} catch (err) { console.error('Agent save error:', err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('saveManualAgent failed:', err); }
}
function createAgentViaAI() {
const desc = document.getElementById('agent-create-input').value.trim();
if (!desc) {
addSystemMessage('Describe the agent you want first.');
return;
}
// Send `/agent create <description>` as a chat prompt β€” the agent_run
// handler routes it through the slash-command expansion + AI generation.
const input = document.getElementById('chat-input');
if (input) {
input.value = `/agent create ${desc}`;
// Trigger send via the global handleSend() (defined elsewhere in this file)
if (typeof handleSend === 'function') {
handleSend();
} else {
const sendBtn = document.getElementById('btn-send');
if (sendBtn) sendBtn.click();
}
} else {
// Fallback: tell user
addSystemMessage(`Type this in chat: /agent create ${desc}`);
}
document.getElementById('agent-create-input').value = '';
}
// ═══════════════════════════════════════════════════════
// GITHUB IMPORT
// ═══════════════════════════════════════════════════════
async function importGithub() {
const urlEl = document.getElementById('github-url-input');
const branchEl = document.getElementById('github-branch-input');
const subdirEl = document.getElementById('github-subdir-input');
const intoEl = document.getElementById('github-into-input');
const statusEl = document.getElementById('github-import-status');
const btn = document.getElementById('btn-import-github');
if (!urlEl) return;
const url = urlEl.value.trim();
if (!url) {
if (statusEl) {
statusEl.style.color = 'var(--red)';
statusEl.textContent = 'Please paste a GitHub URL first.';
}
return;
}
// Basic URL sanity check (server validates too, but quick client-side check first)
if (!/^https:\/\/github\.com\//i.test(url) && !/^git@github\.com:/i.test(url)) {
if (statusEl) {
statusEl.style.color = 'var(--red)';
statusEl.textContent = 'URL must start with https://github.com/ or git@github.com:';
}
return;
}
const branch = (branchEl && branchEl.value.trim()) || '';
const subdir = (subdirEl && subdirEl.value.trim()) || '';
const into = (intoEl && intoEl.value.trim()) || '';
// Disable button + show working state
if (btn) { btn.disabled = true; btn.textContent = '⏳ Cloning...'; }
if (statusEl) {
statusEl.style.color = 'var(--cyan, #00d4ff)';
statusEl.textContent = `Cloning ${url} (depth=1)... this may take 10-60 seconds for large repos.`;
}
try {
const es = await callAgentApi('import_github', [url, branch, subdir, into, '1', '180']);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
if (result.success) {
if (statusEl) {
statusEl.style.color = 'var(--green)';
const treePreview = (result.tree_preview || []).slice(0, 12).join(', ');
statusEl.innerHTML =
`βœ“ ${result.message}` +
(treePreview ? `<br><span style="color:var(--gray-dim);">Top-level: ${treePreview}</span>` : '');
}
addSystemMessage(`πŸ“₯ Imported ${result.owner}/${result.repo} β€” ${result.files_imported} files. Workspace refreshed.`);
// Clear inputs on success
urlEl.value = '';
if (branchEl) branchEl.value = '';
if (subdirEl) subdirEl.value = '';
if (intoEl) intoEl.value = '';
// Refresh workspace tree + todos so the new files are visible
refreshWorkspace();
refreshTodos();
// Optionally auto-enable agent mode so the user can immediately edit
const toggle = document.getElementById('agent-mode-toggle');
if (toggle && !toggle.checked) {
toggle.checked = true;
toggleAgentMode();
}
} else {
if (statusEl) {
statusEl.style.color = 'var(--red)';
statusEl.textContent = `βœ— ${result.message || result.error || 'Import failed.'}`;
}
addSystemMessage(`GitHub import failed: ${result.message || result.error}`);
}
} catch (err) {
console.error('import_github parse error:', err);
if (statusEl) {
statusEl.style.color = 'var(--red)';
statusEl.textContent = 'βœ— Unexpected response from server.';
}
}
if (btn) { btn.disabled = false; btn.textContent = '⬇ Import'; }
es.close();
});
es.addEventListener('error', () => {
if (btn) { btn.disabled = false; btn.textContent = '⬇ Import'; }
if (statusEl) {
statusEl.style.color = 'var(--red)';
statusEl.textContent = 'βœ— Network error during clone.';
}
es.close();
});
} catch (err) {
console.error('importGithub failed:', err);
if (btn) { btn.disabled = false; btn.textContent = '⬇ Import'; }
if (statusEl) {
statusEl.style.color = 'var(--red)';
statusEl.textContent = `βœ— ${err.message || err}`;
}
}
}
// Allow pressing Enter in the URL input to trigger import
function setupGithubImportEnterKey() {
const urlEl = document.getElementById('github-url-input');
if (urlEl && !urlEl.dataset.enterBound) {
urlEl.dataset.enterBound = '1';
urlEl.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') { ev.preventDefault(); importGithub(); }
});
}
}
async function refreshTodos() {
try {
const es = await callAgentApi('todo_read', ['default']);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
state.todos = result.todos || [];
renderTodos();
} catch (err) { console.error('Todo refresh error:', err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('refreshTodos failed:', err); }
}
function renderTodos() {
const container = document.getElementById('todos-display');
if (!container) return;
if (state.todos.length === 0) {
container.innerHTML = '<div style="color:var(--gray-dim);">No todos yet. Use the agent to create some.</div>';
return;
}
const statusColor = { pending: 'var(--gray-mid)', in_progress: 'var(--amber)', completed: 'var(--green)' };
const statusIcon = { pending: 'β—‹', in_progress: '◐', completed: '●' };
container.innerHTML = state.todos.map(t =>
`<div style="padding:4px 0;border-bottom:1px solid var(--border);">
<span style="color:${statusColor[t.status] || 'var(--gray-mid)'};">${statusIcon[t.status] || 'β—‹'}</span>
<span style="margin-left:6px;color:var(--gray-light);">[${t.priority || 'med'}] ${t.content || t.id}</span>
</div>`
).join('');
}
async function refreshWorkspace() {
try {
const es = await callAgentApi('workspace_tree', []);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
if (result.success) {
renderWorkspaceTree(result.tree);
}
} catch (err) { console.error('Workspace refresh error:', err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('refreshWorkspace failed:', err); }
}
function renderWorkspaceTree(node, depth = 0) {
const container = document.getElementById('workspace-tree');
if (!container) return;
if (depth === 0) {
if (!node || (node.children || []).length === 0) {
container.innerHTML = '<div style="color:var(--gray-dim);">Empty. The agent will create files here.</div>';
return;
}
container.innerHTML = renderWorkspaceNode(node, 0);
}
}
function renderWorkspaceNode(node, depth) {
const indent = '&nbsp;'.repeat(depth * 2);
if (node.type === 'dir') {
const children = (node.children || []).map(c => renderWorkspaceNode(c, depth + 1)).join('');
return `<div>${indent}<span style="color:var(--cyan);">πŸ“ ${node.name}</span></div>${children}`;
} else {
const size = node.size ? `${(node.size / 1024).toFixed(1)}KB` : '';
return `<div>${indent}<span style="color:var(--gray-light);">πŸ“„ ${node.name}</span> <span style="color:var(--gray-dim);font-size:10px;">${size}</span></div>`;
}
}
async function resetWorkspace() {
if (!confirm('Reset the workspace? All files will be deleted.')) return;
try {
const es = await callAgentApi('workspace_reset', []);
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
const result = JSON.parse(data[0]);
addSystemMessage('Workspace cleared.');
refreshWorkspace();
} catch (err) { console.error(err); }
es.close();
});
es.addEventListener('error', () => es.close());
} catch (err) { console.error('resetWorkspace failed:', err); }
}
// Override sendMessage to support agent mode
const originalSendMessage = sendMessage;
async function sendMessageAgent(prompt) {
if (state.isGenerating) return;
if (!state.modelReady) {
addSystemMessage('The model is still loading. Please wait...');
return;
}
state.isGenerating = true;
toggleInputState(true);
addUserMessage(prompt);
addAssistantMessage();
renderStatus('Running agent...', 'working');
const historyJSON = JSON.stringify(state.history.slice(0, -2)); // exclude just-added user+assistant placeholders
const framework = document.getElementById('framework-select')?.value || state.targetFramework;
const skillsJSON = JSON.stringify(state.activeSkills || []);
try {
const resp = await fetch('/gradio_api/call/agent_run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [
prompt,
state.targetLanguage,
framework,
historyJSON,
skillsJSON,
state.searchEnabled ? 'true' : 'false',
state.uploadedImageFileUrl || '',
state.activeAgent || ''
]
})
});
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/agent_run/${event_id}`);
state.currentEventSource = eventSource;
let lastContent = '';
eventSource.addEventListener('generating', (e) => {
try {
const dataArray = JSON.parse(e.data);
const event = JSON.parse(dataArray[0]);
handleAgentEvent(event);
} catch (err) { console.error('Parse error (generating):', err); }
});
eventSource.addEventListener('complete', (e) => {
try {
const dataArray = JSON.parse(e.data);
const event = JSON.parse(dataArray[0]);
handleAgentEvent(event);
} catch (err) { console.error('Parse error (complete):', err); }
eventSource.close();
onGenerationEnd();
// Refresh workspace + todos after agent run
refreshWorkspace();
refreshTodos();
});
eventSource.addEventListener('error', (e) => {
let errorMsg = 'Agent error.';
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('Agent send error:', err);
finalizeAssistantMessage();
addSystemMessage(`Error: ${err.message}`);
renderStatus('Error', 'error');
onGenerationEnd();
}
}
function handleAgentEvent(event) {
if (!event || typeof event !== 'object') return;
switch (event.type) {
case 'status':
renderStatus(event.status_text || 'Working...', event.status_state || 'working');
break;
case 'streaming':
// Update the current assistant message with streaming content
const msgs = document.querySelectorAll('.chat-message.assistant .message-content');
const last = msgs[msgs.length - 1];
if (last) {
last.innerHTML = window.renderMarkdown ? renderMarkdown(event.content || '') : (event.content || '').replace(/\n/g, '<br>');
}
// Update history
if (state.history.length > 0 && state.history[state.history.length - 1].role === 'assistant') {
state.history[state.history.length - 1].content = event.content || '';
}
renderStatus(`Thinking (step ${event.iteration || 1})...`, 'working');
break;
case 'tool_call':
addSystemMessage(`πŸ”§ Calling tool: ${event.tool}` + (event.args && Object.keys(event.args).length ? `(${JSON.stringify(event.args).slice(0, 200)})` : ''));
renderStatus(`Running tool: ${event.tool}...`, 'working');
break;
case 'tool_result':
const result = event.result || {};
const success = result.success !== false;
const summary = success ? 'βœ“' : 'βœ—';
let resultPreview = '';
if (result.stdout) resultPreview = result.stdout.slice(0, 200);
else if (result.content) resultPreview = result.content.slice(0, 200);
else if (result.error) resultPreview = result.error;
else if (result.entries) resultPreview = `${result.entries.length} entries`;
else if (result.matches) resultPreview = `${result.matches.length} matches`;
else if (result.count !== undefined) resultPreview = `${result.count} items`;
else resultPreview = JSON.stringify(result).slice(0, 200);
// Show hook warnings if any
if (result.hook_warnings && result.hook_warnings.length > 0) {
for (const w of result.hook_warnings) {
addSystemMessage(`⚠️ Hook: ${w.slice(0, 300)}`);
}
}
addSystemMessage(`${summary} ${event.tool}: ${resultPreview}${resultPreview.length >= 200 ? '...' : ''}`);
break;
case 'search_results':
state.currentSearchResults = event.results || [];
renderStatus(`Found ${event.results?.length || 0} results, running agent...`, 'working');
break;
case 'complete':
// Finalize the assistant message
const content = event.content || '';
if (state.history.length > 0 && state.history[state.history.length - 1].role === 'assistant') {
state.history[state.history.length - 1].content = content;
}
finalizeAssistantMessage();
// Try to extract code from the response
tryExtractCodeFromResponse(content);
renderStatus('Done', 'success');
break;
case 'error':
finalizeAssistantMessage();
addSystemMessage(`Error: ${event.message || 'Unknown error'}`);
if (event.available && event.available.length) {
addSystemMessage('Available: ' + event.available.join(', '));
}
renderStatus('Error', 'error');
break;
}
}
function tryExtractCodeFromResponse(content) {
// Reuse the existing code extraction logic by simulating a chat payload
if (!content) return;
// Strip tool call blocks for display
const cleanContent = content.replace(/```tool\s*\n.*?```/gs, '').trim();
if (!cleanContent) return;
// Try to extract code blocks
const codeMatch = cleanContent.match(/```([a-zA-Z0-9_+.#-]*)\s*\n(.*?)```/s);
if (codeMatch) {
const code = codeMatch[2].trim();
const lang = codeMatch[1].toLowerCase();
state.lastCode = code;
state.lastCodeLang = lang;
// Update code tab
const codeDisplay = document.getElementById('code-display');
if (codeDisplay) {
codeDisplay.innerHTML = `<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>`;
}
document.getElementById('code-tab-lang').textContent = lang || 'text';
document.getElementById('btn-download').style.display = 'inline-block';
// If HTML, show in preview
if (lang === 'html' || /^<!doctype|<html/i.test(code)) {
const iframe = document.getElementById('preview-iframe');
if (iframe) {
iframe.srcdoc = code;
document.getElementById('preview-placeholder').style.display = 'none';
document.getElementById('preview-image').style.display = 'none';
iframe.style.display = 'block';
}
}
}
// Try multi-file extraction
const fileMatch = cleanContent.match(/@@FILE:\s*(.+?)@@\s*\n(.*?)(?=@@FILE:|@@END@@)/s);
if (fileMatch) {
// Build project files
const files = {};
const fileRegex = /@@FILE:\s*(.+?)@@\s*\n(.*?)(?=@@FILE:|@@END@@)/gs;
let m;
while ((m = fileRegex.exec(cleanContent)) !== null) {
files[m[1].trim()] = m[2].trim();
}
if (Object.keys(files).length > 0) {
state.executionContext = state.executionContext || {};
state.executionContext.project_files = files;
// Update project files display in deploy tab
const pf = document.getElementById('project-files');
if (pf) {
pf.innerHTML = '<div style="font-size:11px;color:var(--gray-mid);margin-top:8px;">' +
Object.keys(files).map(f => `<div>πŸ“„ ${f}</div>`).join('') +
'</div>';
}
}
}
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
// Override sendMessage to route to agent when agentMode is on
sendMessage = async function(prompt) {
if (state.agentMode) {
return sendMessageAgent(prompt);
}
return originalSendMessage(prompt);
};
// Initialize agent UI on page load
document.addEventListener('DOMContentLoaded', () => {
// Render skills/commands/hooks/agents after CONFIG is loaded
setTimeout(() => {
renderSkillsList();
renderCommandsList();
renderHooksList();
renderActiveSkills();
// Pull the live agent list + active agent from the backend
refreshAgents();
renderAgentsList();
renderActiveAgentBadge();
// Wire up Enter-key handler on the GitHub import URL input
setupGithubImportEnterKey();
}, 100);
});
function resetConversation(announcement) {
state.history = [];
state.executionContext = {};
state.lastExecution = null;
state.lastCode = '';
state.lastCodeLang = '';
state.reasoningExpanded = false;
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');
if (announcement) addSystemMessage(announcement);
}
function newChat() {
resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'SoniCoder'}.`);
}
function toggleThinking() {
state.showThinking = !state.showThinking;
const btn = document.getElementById('btn-thinking');
if (state.showThinking) {
btn.classList.add('active');
document.body.classList.remove('hide-thinking');
btn.textContent = '🧠 Think';
} else {
btn.classList.remove('active');
document.body.classList.add('hide-thinking');
btn.textContent = '🧠 Think';
}
}
// ═══════════════════════════════════════════════════════
// MODEL SWITCHING
// ═══════════════════════════════════════════════════════
async function onModelChange() {
const select = document.getElementById('model-select');
const modelKey = select.value;
if (modelKey === state.currentModelKey) return;
const isVLM = modelKey === 'minicpm-v-4.6';
const imageGroup = document.getElementById('image-attach-group');
if (isVLM) {
imageGroup.style.display = 'flex';
} else {
imageGroup.style.display = 'none';
removeImage();
}
addSystemMessage(`Switching model to ${select.options[select.selectedIndex].text}...`);
renderStatus('Switching model...', 'working');
try {
const resp = await fetch('/gradio_api/call/switch_model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: [modelKey] })
});
const { event_id } = await resp.json();
const eventSource = new EventSource(`/gradio_api/call/switch_model/${event_id}`);
eventSource.addEventListener('complete', (e) => {
const dataArray = JSON.parse(e.data);
const result = JSON.parse(dataArray[0]);
if (result.success) {
state.currentModelKey = modelKey;
state.currentModelType = isVLM ? 'vlm' : 'text';
const name = isVLM ? 'MiniCPM-V-4.6' : 'MiniCPM5-1B';
document.getElementById('model-pill-text').textContent = name;
document.getElementById('header-model-name').textContent = name;
addSystemMessage(`Switched to ${name}. Model is loading in background...`);
// Poll for model ready
pollModelStatus();
} else {
addSystemMessage(`Failed to switch: ${result.message}`);
select.value = state.currentModelKey;
}
eventSource.close();
});
eventSource.addEventListener('error', () => {
addSystemMessage('Model switch failed');
select.value = state.currentModelKey;
eventSource.close();
});
} catch (err) {
addSystemMessage(`Switch error: ${err.message}`);
select.value = state.currentModelKey;
}
}
function pollModelStatus() {
const interval = setInterval(async () => {
try {
const resp = await fetch('/api/model-status');
const status = await resp.json();
if (status.status === 'ready') {
state.modelReady = true;
const dot = document.getElementById('model-dot');
dot.className = 'dot';
dot.style.background = 'var(--success)';
dot.style.boxShadow = '0 0 6px var(--success)';
renderStatus('Ready', 'success');
setTimeout(() => renderStatus('Idle', 'idle'), 2000);
clearInterval(interval);
} else if (status.status === 'error') {
state.modelReady = false;
renderStatus('Model error', 'error');
clearInterval(interval);
}
} catch { clearInterval(interval); }
}, 2000);
}
// ═══════════════════════════════════════════════════════
// IMAGE UPLOAD (VLM)
// ═══════════════════════════════════════════════════════
async function onImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
state.uploadedImageName = file.name;
document.getElementById('image-attach-name').textContent = file.name;
document.getElementById('btn-remove-image').style.display = 'inline';
// Convert to base64
const reader = new FileReader();
reader.onload = async function(e) {
const base64Data = e.target.result;
// Upload to server
try {
const resp = await fetch('/gradio_api/call/upload_image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: [base64Data] })
});
const { event_id } = await resp.json();
const eventSource = new EventSource(`/gradio_api/call/upload_image/${event_id}`);
eventSource.addEventListener('complete', (ev) => {
const dataArray = JSON.parse(ev.data);
const result = JSON.parse(dataArray[0]);
if (result.success) {
state.uploadedImageFileUrl = result.file_url;
document.getElementById('image-attach-name').textContent = 'βœ“ ' + file.name;
} else {
document.getElementById('image-attach-name').textContent = 'βœ— Upload failed';
}
eventSource.close();
});
eventSource.addEventListener('error', () => {
document.getElementById('image-attach-name').textContent = 'βœ— Upload error';
eventSource.close();
});
} catch (err) {
document.getElementById('image-attach-name').textContent = 'βœ— Error';
}
};
reader.readAsDataURL(file);
}
function removeImage() {
state.uploadedImageFileUrl = '';
state.uploadedImageName = '';
document.getElementById('image-upload').value = '';
document.getElementById('image-attach-name').textContent = '';
document.getElementById('btn-remove-image').style.display = 'none';
}
// ═══════════════════════════════════════════════════════
// WEB SEARCH
// ═══════════════════════════════════════════════════════
async function doWebSearch() {
const query = document.getElementById('search-input').value.trim();
if (!query) return;
const resultsContainer = document.getElementById('search-results');
resultsContainer.innerHTML = '<div class="search-results-empty" style="color:var(--amber);">Searching...</div>';
try {
const resp = await fetch('/gradio_api/call/web_search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: [query] })
});
const { event_id } = await resp.json();
const eventSource = new EventSource(`/gradio_api/call/web_search/${event_id}`);
eventSource.addEventListener('complete', (e) => {
const dataArray = JSON.parse(e.data);
const result = JSON.parse(dataArray[0]);
if (result.success) {
state.lastSearchResults = result.results;
renderSearchResults(result.results);
} else {
resultsContainer.innerHTML = `<div class="search-results-empty">${escapeHtml(result.message)}</div>`;
}
eventSource.close();
});
eventSource.addEventListener('error', (e) => {
resultsContainer.innerHTML = '<div class="search-results-empty">Search failed</div>';
eventSource.close();
});
} catch (err) {
resultsContainer.innerHTML = `<div class="search-results-empty">Error: ${err.message}</div>`;
}
}
function searchAndGenerate() {
const input = document.getElementById('chat-input');
const prompt = input.value.trim();
if (!prompt || state.isGenerating) return;
input.value = '';
autoResize();
state.searchEnabled = true;
sendMessage(prompt);
// Reset after sending
state.searchEnabled = false;
}
function renderSearchResults(results) {
const container = document.getElementById('search-results');
if (!results || results.length === 0) {
container.innerHTML = '<div class="search-results-empty">No results found.</div>';
return;
}
let html = '';
results.forEach((r) => {
html += `<div class="search-result-item">
<a class="search-result-title" href="${escapeHtml(r.url)}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a>
<span class="search-result-url">${escapeHtml(r.url)}</span>
<div class="search-result-snippet">${escapeHtml(r.snippet)}</div>
</div>`;
});
container.innerHTML = html;
}
// ═══════════════════════════════════════════════════════
// SEARCH SOURCE BADGE (Grok-style inline in chat)
// ═══════════════════════════════════════════════════════
function renderSearchSourceBadge(results, expanded) {
const div = document.getElementById('current-assistant-msg');
if (!div) return;
const container = div.querySelector('.search-source-container');
if (!container || !results || results.length === 0) return;
const count = results.length;
const openClass = expanded ? ' open' : '';
// Build source items HTML
let sourceItems = '';
results.forEach((r) => {
let domain = '';
try { domain = new URL(r.url).hostname.replace('www.', ''); } catch(e) { domain = r.host_name || r.url; }
const faviconUrl = r.favicon || `https://www.google.com/s2/favicons?domain=${domain}&sz=16`;
sourceItems += `<div class="source-item">
<img class="source-favicon" src="${escapeHtml(faviconUrl)}" alt="" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="source-favicon-placeholder" style="display:none;">${escapeHtml(domain.charAt(0).toUpperCase())}</div>
<div class="source-info">
<a class="source-title" href="${escapeHtml(r.url)}" target="_blank" rel="noopener" onclick="event.stopPropagation();">${escapeHtml(r.title)}</a>
<span class="source-domain">${escapeHtml(domain)}</span>
<div class="source-snippet">${escapeHtml(r.snippet)}</div>
</div>
</div>`;
});
container.innerHTML = `
<div class="search-source-badge${openClass}" onclick="toggleSearchPanel(this)">
<span class="badge-icon">&#128269;</span>
<span>Searched</span>
<span class="badge-count">${count} source${count !== 1 ? 's' : ''}</span>
<span class="badge-arrow">&#9660;</span>
</div>
<div class="search-source-panel${openClass}">
${sourceItems}
</div>
`;
scrollToBottom();
}
function toggleSearchPanel(badge) {
const panel = badge.nextElementSibling;
const isOpen = badge.classList.contains('open');
badge.classList.toggle('open', !isOpen);
panel.classList.toggle('open', !isOpen);
state.searchPanelExpanded = !isOpen;
scrollToBottom();
}
// ═══════════════════════════════════════════════════════
// HUGGINGFACE OAUTH + PUSH
// ═══════════════════════════════════════════════════════
// OAuth state
state.hfAuth = {
authenticated: false,
token: '',
username: '',
name: '',
picture: '',
organizations: [],
};
async function loginWithHF() {
// On HuggingFace Spaces, Gradio handles OAuth via /login/huggingface
// We redirect to the Gradio OAuth endpoint
const currentUrl = window.location.href;
const loginUrl = `/login/huggingface?callback_url=${encodeURIComponent(currentUrl)}`;
window.location.href = loginUrl;
}
function logoutHF() {
state.hfAuth = {
authenticated: false,
token: '',
username: '',
name: '',
picture: '',
organizations: [],
};
// Update UI
document.getElementById('btn-hf-login').style.display = '';
document.getElementById('hf-user-info').style.display = 'none';
document.getElementById('hf-manual-token-section').style.display = '';
document.getElementById('hf-auth-hint').textContent = 'Sign in with OAuth β€” no token paste needed';
// Reset owner dropdown
const ownerSelect = document.getElementById('hf-owner');
ownerSelect.innerHTML = '<option value="">Sign in to see options</option>';
// Check for Gradio OAuth token in session
checkGradioOAuth();
}
async function checkGradioOAuth() {
// Try to get the OAuth token from Gradio's session
// Gradio stores the token in cookies/session after OAuth login
try {
// Check if we have a Gradio session with OAuth token
const resp = await fetch('/gradio_api/call/hf_auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: [''] })
});
if (!resp.ok) return;
const { event_id } = await resp.json();
const eventSource = new EventSource(`/gradio_api/call/hf_auth/${event_id}`);
eventSource.addEventListener('complete', (e) => {
try {
const dataArray = JSON.parse(e.data);
const result = JSON.parse(dataArray[0]);
if (result.authenticated) {
handleAuthResult(result);
}
} catch (err) { /* not authenticated */ }
eventSource.close();
});
eventSource.addEventListener('error', () => { eventSource.close(); });
} catch (err) {
// OAuth not available β€” show manual token input
}
}
function handleAuthResult(result) {
state.hfAuth = {
authenticated: result.authenticated,
token: result.token || '',
username: result.username || '',
name: result.name || '',
picture: result.picture || '',
organizations: result.organizations || [],
};
if (result.authenticated) {
// Update UI β€” show user info
document.getElementById('btn-hf-login').style.display = 'none';
document.getElementById('hf-user-info').style.display = '';
document.getElementById('hf-user-name').textContent = result.name || result.username;
if (result.picture) {
document.getElementById('hf-user-avatar').src = result.picture;
document.getElementById('hf-user-avatar').style.display = '';
} else {
document.getElementById('hf-user-avatar').style.display = 'none';
}
document.getElementById('hf-auth-hint').textContent = `Signed in as ${result.username}`;
document.getElementById('hf-manual-token-section').style.display = 'none';
// Populate owner dropdown
const ownerSelect = document.getElementById('hf-owner');
ownerSelect.innerHTML = '';
// Add user as first option
const userOpt = document.createElement('option');
userOpt.value = result.username;
userOpt.textContent = `${result.username} (you)`;
ownerSelect.appendChild(userOpt);
// Add organizations
if (result.organizations && result.organizations.length > 0) {
result.organizations.forEach(org => {
const opt = document.createElement('option');
opt.value = org.name;
const roleSuffix = org.role ? ` (${org.role})` : '';
opt.textContent = `${org.name}${roleSuffix}`;
ownerSelect.appendChild(opt);
});
}
// Auto-fill token in hidden field for push
document.getElementById('hf-token').value = result.token;
}
}
async function pushToHuggingFace() {
const ownerSelect = document.getElementById('hf-owner');
const owner = ownerSelect.value;
const repoName = document.getElementById('hf-repo-name').value.trim();
const hfToken = document.getElementById('hf-token').value.trim();
const spaceSdk = document.getElementById('hf-space-sdk').value;
const statusEl = document.getElementById('deploy-status');
// Build full repo name
let fullRepoName = repoName;
if (owner && repoName && !repoName.includes('/')) {
fullRepoName = `${owner}/${repoName}`;
}
if (!fullRepoName) {
statusEl.className = 'deploy-status error';
statusEl.textContent = 'Please enter a repository name.';
statusEl.style.display = 'block';
return;
}
if (!hfToken) {
statusEl.className = 'deploy-status error';
statusEl.textContent = 'Please sign in with HuggingFace or enter a token.';
statusEl.style.display = 'block';
return;
}
if (!state.executionContext || !state.executionContext.code) {
statusEl.className = 'deploy-status error';
statusEl.textContent = 'No code to push. Generate some code first.';
statusEl.style.display = 'block';
return;
}
statusEl.className = 'deploy-status working';
statusEl.textContent = 'Pushing to HuggingFace...';
statusEl.style.display = 'block';
const btn = document.getElementById('btn-push-hf');
btn.disabled = true;
try {
const execContextJSON = JSON.stringify(state.executionContext);
const resp = await fetch('/gradio_api/call/push_hf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [execContextJSON, fullRepoName, hfToken, spaceSdk, 'true']
})
});
if (!resp.ok) throw new Error(`API error: ${resp.status}`);
const { event_id } = await resp.json();
const eventSource = new EventSource(`/gradio_api/call/push_hf/${event_id}`);
eventSource.addEventListener('complete', (e) => {
try {
const dataArray = JSON.parse(e.data);
const result = JSON.parse(dataArray[0]);
if (result.success) {
statusEl.className = 'deploy-status success';
statusEl.innerHTML = `\u2713 ${result.message}<br><a href="${result.url}" target="_blank" rel="noopener">${result.url} \u2197</a>`;
} else {
statusEl.className = 'deploy-status error';
statusEl.textContent = `\u2717 ${result.message}`;
}
} catch (err) {
statusEl.className = 'deploy-status error';
statusEl.textContent = `Parse error: ${err.message}`;
}
eventSource.close();
btn.disabled = false;
});
eventSource.addEventListener('error', (e) => {
statusEl.className = 'deploy-status error';
statusEl.textContent = `Push failed: ${e.data || 'Unknown error'}`;
eventSource.close();
btn.disabled = false;
});
} catch (err) {
statusEl.className = 'deploy-status error';
statusEl.textContent = `Push failed: ${err.message}`;
btn.disabled = false;
}
}
// ═══════════════════════════════════════════════════════
// PUSH TO GITHUB (3 inputs: repo name, token, username)
// ═══════════════════════════════════════════════════════
async function pushToGithub() {
const repoNameEl = document.getElementById('gh-repo-name');
const tokenEl = document.getElementById('gh-token');
const usernameEl = document.getElementById('gh-username');
const branchEl = document.getElementById('gh-branch');
const msgEl = document.getElementById('gh-commit-msg');
const statusEl = document.getElementById('github-deploy-status');
const btn = document.getElementById('btn-push-github');
if (!repoNameEl || !tokenEl || !usernameEl || !statusEl || !btn) return;
const repoName = repoNameEl.value.trim();
const token = tokenEl.value.trim();
const username = usernameEl.value.trim();
const branch = (branchEl && branchEl.value.trim()) || 'main';
const commitMsg = (msgEl && msgEl.value.trim()) || '';
// ── Validate the 3 required inputs ───────────────────────────────
if (!repoName) {
statusEl.className = 'deploy-status error';
statusEl.textContent = 'Please enter the repository name.';
statusEl.style.display = 'block';
repoNameEl.focus();
return;
}
if (!token) {
statusEl.className = 'deploy-status error';
statusEl.textContent = 'Please enter your GitHub API token.';
statusEl.style.display = 'block';
tokenEl.focus();
return;
}
if (!username) {
statusEl.className = 'deploy-status error';
statusEl.textContent = 'Please enter your GitHub username.';
statusEl.style.display = 'block';
usernameEl.focus();
return;
}
// Quick client-side token sanity check (server validates too)
if (token.length < 20) {
statusEl.className = 'deploy-status error';
statusEl.textContent = 'Token looks too short β€” expected a GitHub PAT (starts with ghp_, github_pat_, etc.).';
statusEl.style.display = 'block';
return;
}
// Confirm push β€” it overwrites the remote tip
const fullRepo = repoName.includes('/') ? repoName : `${username}/${repoName}`;
if (!confirm(
`Push workspace to github.com/${fullRepo} on branch "${branch}"?\n\n` +
`This will overwrite the remote tip (--force-with-lease).\n` +
`Token will be sent to the SoniCoder backend only (not stored).`
)) {
return;
}
// ── Disable + show working state ─────────────────────────────────
btn.disabled = true;
btn.textContent = '⏳ Pushing...';
statusEl.className = 'deploy-status working';
statusEl.textContent = `Pushing to github.com/${fullRepo} on "${branch}"... (this may take 10-60 seconds)`;
statusEl.style.display = 'block';
try {
const resp = await fetch('/gradio_api/call/push_github', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [repoName, token, username, branch, commitMsg, '180']
})
});
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/push_github/${event_id}`);
eventSource.addEventListener('complete', (e) => {
try {
const dataArray = JSON.parse(e.data);
const result = JSON.parse(dataArray[0]);
if (result.success) {
statusEl.className = 'deploy-status success';
const sha = result.commit_sha ? ` (commit ${result.commit_sha.slice(0,8)})` : '';
const commitLink = result.commit_url
? `<br><a href="${result.commit_url}" target="_blank" rel="noopener">view commit ${result.commit_sha ? result.commit_sha.slice(0,8) : ''} &nearr;</a>`
: '';
const repoLink = result.repo_url
? `<br><a href="${result.repo_url}" target="_blank" rel="noopener">${result.repo_url} &nearr;</a>`
: '';
statusEl.innerHTML =
`βœ“ ${result.message}${sha}${commitLink}${repoLink}`;
addSystemMessage(`πŸ“¦ Pushed ${result.files_pushed} file(s) to ${result.repo_full_name}.`);
} else {
statusEl.className = 'deploy-status error';
statusEl.textContent = `βœ— ${result.message || result.error || 'Push failed.'}`;
}
} catch (err) {
statusEl.className = 'deploy-status error';
statusEl.textContent = `Parse error: ${err.message}`;
}
eventSource.close();
btn.disabled = false;
btn.textContent = 'πŸ“¦ Push to GitHub';
});
eventSource.addEventListener('error', (e) => {
statusEl.className = 'deploy-status error';
statusEl.textContent = `Push failed: ${e.data || 'Network error. Check console.'}`;
eventSource.close();
btn.disabled = false;
btn.textContent = 'πŸ“¦ Push to GitHub';
});
} catch (err) {
statusEl.className = 'deploy-status error';
statusEl.textContent = `Push failed: ${err.message}`;
btn.disabled = false;
btn.textContent = 'πŸ“¦ Push to GitHub';
}
}
</script>
</body>
</html>