Spaces:
Running
Running
fix: agent_run param mismatch (send agent_name) + add GitHub push-update (3 inputs: repo name, token, username; --force-with-lease)
0df4996 verified | <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; } | |
| /* 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 ; } | |
| #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 ; } | |
| #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">╔═══ FULLSTACK CODE BUILDER ═══╚</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 — 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">❯</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">🔍</button> | |
| <button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">➤</button> | |
| <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">■ STOP</button> | |
| </div> | |
| <div id="examples-row"></div> | |
| </div> | |
| </div> | |
| <!-- Output Panel --> | |
| <div id="output-panel"> | |
| <div id="output-tabs"> | |
| <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button> | |
| <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button> | |
| <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button> | |
| <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"> | |
| ┌─────────────────────┐ | |
| │ ╱━━━╲ │ | |
| │ │ ▶ │ OUTPUT │ | |
| │ ╵━━━╴ │ | |
| └─────────────────────┘</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()">⥊ FULLSCREEN</button> | |
| </div> | |
| <!-- Console Pane --> | |
| <div class="tab-pane" id="pane-console"> | |
| <div class="console-section"> | |
| <div class="console-label">stdout:</div> | |
| <div class="console-output" id="console-stdout">No output yet.</div> | |
| </div> | |
| <div class="console-section"> | |
| <div class="console-label">stderr:</div> | |
| <div class="console-output" id="console-stderr">No errors.</div> | |
| </div> | |
| </div> | |
| <!-- Code Pane --> | |
| <div class="tab-pane" id="pane-code"> | |
| <div class="code-tab-header"> | |
| <span class="code-tab-lang" id="code-tab-lang">—</span> | |
| <div class="code-tab-actions"> | |
| <button class="code-tab-btn" id="btn-copy-code" onclick="copyCode()">📋 Copy</button> | |
| <a class="code-tab-btn" id="btn-download" href="#" style="display:none;">⬇ Download</a> | |
| </div> | |
| </div> | |
| <div id="code-display"> | |
| <div class="code-placeholder">No code generated yet.</div> | |
| </div> | |
| </div> | |
| <!-- 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()">🔍 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">🤖 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>🤖 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;">✨ 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;">✨ 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 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);">📢 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 <url></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;">⬇ 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">🚀 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()">🤝 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>🚀 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);">📦 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 ↗</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;">📦 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">●</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()">[✕ 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></span><span class="msg-content">${escapeHtml(text)}</span>`; | |
| container.appendChild(div); | |
| scrollToBottom(); | |
| } | |
| function addUserMessage(text) { | |
| const container = document.getElementById('chat-messages'); | |
| const div = document.createElement('div'); | |
| div.className = 'msg msg-user'; | |
| div.innerHTML = `<span class="msg-prefix">user></span><span class="msg-content">${escapeHtml(text)}</span>`; | |
| container.appendChild(div); | |
| scrollToBottom(); | |
| } | |
| function addAssistantMessage() { | |
| const container = document.getElementById('chat-messages'); | |
| const div = document.createElement('div'); | |
| div.className = 'msg msg-assistant'; | |
| div.id = 'current-assistant-msg'; | |
| div.innerHTML = `<span class="msg-prefix">ai></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}')">📋 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 = ' '.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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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">🔍</span> | |
| <span>Searched</span> | |
| <span class="badge-count">${count} source${count !== 1 ? 's' : ''}</span> | |
| <span class="badge-arrow">▼</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) : ''} ↗</a>` | |
| : ''; | |
| const repoLink = result.repo_url | |
| ? `<br><a href="${result.repo_url}" target="_blank" rel="noopener">${result.repo_url} ↗</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> | |