sonicoder / index.html
R-Kentaren's picture
Upload folder using huggingface_hub
c4c43f1 verified
raw
history blame contribute delete
99.1 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&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; }
/* 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; gap: 14px; overflow-y: 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="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>
<!-- Deploy Pane -->
<div class="tab-pane" id="pane-deploy">
<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>
</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: '',
};
// ═══════════════════════════════════════════════════════
// 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. The model is loading locally (no API keys needed). Select a language and framework, then 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();
}
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;
}
}
</script>
</body>
</html>