| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Fullstack Code Builder</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> |
| |
| |
| |
| *, *::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 { |
| display: flex; |
| flex-direction: column; |
| height: 100vh; |
| max-height: 100vh; |
| } |
| |
| |
| |
| |
| #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); |
| } |
| |
| |
| |
| |
| #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 { |
| display: flex; |
| flex: 1; |
| min-height: 0; |
| overflow: hidden; |
| } |
| |
| |
| |
| |
| #terminal-panel { |
| display: flex; |
| flex-direction: column; |
| flex: 1; |
| min-width: 0; |
| border-right: 1px solid var(--border); |
| } |
| |
| .panel-label { |
| padding: 6px 16px; |
| font-size: 10px; |
| letter-spacing: 2px; |
| color: var(--gray-dim); |
| border-bottom: 1px solid var(--border); |
| background: rgba(13,17,23,0.6); |
| text-transform: uppercase; |
| flex-shrink: 0; |
| } |
| |
| #chat-messages { |
| flex: 1; |
| overflow-y: auto; |
| padding: 16px; |
| scroll-behavior: smooth; |
| } |
| |
| |
| .msg { |
| margin-bottom: 14px; |
| line-height: 1.65; |
| animation: msgFadeIn 0.25s ease; |
| } |
| |
| @keyframes msgFadeIn { |
| from { opacity: 0; transform: translateY(6px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .msg-prefix { font-weight: 600; margin-right: 4px; } |
| .msg-user .msg-prefix { color: var(--green); text-shadow: var(--glow-green); } |
| .msg-user .msg-content { color: var(--green); text-shadow: var(--glow-green); } |
| .msg-assistant .msg-prefix { color: var(--cyan); text-shadow: var(--glow-cyan); 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; } |
| |
| |
| .msg-content strong { color: #fff; font-weight: 600; } |
| .msg-content em { font-style: italic; color: var(--gray-mid); } |
| .msg-content code:not(pre code) { |
| background: var(--bg-code); |
| color: var(--code-text); |
| padding: 1px 5px; |
| border-radius: 3px; |
| font-size: 12px; |
| border: 1px solid var(--border); |
| } |
| .msg-content a { color: var(--cyan); } |
| .msg-content ul, .msg-content ol { margin: 6px 0 6px 20px; } |
| .msg-content li { margin-bottom: 3px; } |
| .msg-content h1, .msg-content h2, .msg-content h3 { |
| color: var(--cyan); margin: 10px 0 6px; font-size: 14px; text-shadow: var(--glow-cyan); |
| } |
| .msg-content h1 { font-size: 16px; } |
| .msg-content h2 { font-size: 15px; } |
| .msg-content p { margin: 4px 0; } |
| |
| |
| .code-block-wrap { |
| margin: 8px 0; |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| overflow: hidden; |
| background: var(--bg-code); |
| } |
| .code-block-header { |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 4px 10px; background: rgba(30,42,58,0.5); |
| border-bottom: 1px solid var(--border); font-size: 11px; |
| } |
| .code-lang { color: var(--amber); text-transform: uppercase; letter-spacing: 1px; } |
| .btn-copy { |
| background: transparent; border: 1px solid var(--border); color: var(--gray-mid); |
| font-family: var(--font-mono); font-size: 10px; padding: 2px 8px; |
| border-radius: 3px; cursor: pointer; transition: all var(--transition); |
| } |
| .btn-copy:hover { border-color: var(--green); color: var(--green); } |
| .btn-copy.copied { border-color: var(--success); color: var(--success); } |
| .code-block-wrap pre { |
| margin: 0; padding: 10px 12px; overflow-x: auto; |
| font-size: 12px; line-height: 1.5; color: var(--code-text); |
| } |
| .code-block-wrap pre code { font-family: var(--font-mono); background: none; border: none; padding: 0; } |
| |
| |
| .think-block { |
| margin: 8px 0; border: 1px solid rgba(255,179,0,0.15); |
| border-radius: var(--radius); background: rgba(255,179,0,0.03); |
| } |
| .think-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; } |
| |
| |
| body.hide-thinking .think-block { display: none; } |
| |
| |
| .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::after { |
| content: '\u2588'; animation: blink 0.8s step-end infinite; |
| color: var(--green); margin-left: 2px; |
| } |
| @keyframes blink { 50% { opacity: 0; } } |
| |
| |
| |
| |
| #input-area { |
| flex-shrink: 0; |
| border-top: 1px solid var(--border); |
| background: var(--bg-panel); |
| padding: 10px 16px 8px; |
| } |
| |
| |
| #target-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-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 { |
| 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-pane { display: none; height: 100%; min-height: 0; } |
| .tab-pane.active { display: flex; flex-direction: column; } |
| |
| |
| #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); } |
| |
| |
| #pane-console { padding: 12px 16px; gap: 12px; overflow-y: auto; } |
| |
| .console-section { margin-bottom: 8px; } |
| .console-label { |
| font-size: 10px; letter-spacing: 2px; color: var(--gray-dim); |
| margin-bottom: 4px; text-transform: uppercase; |
| } |
| .console-output { |
| background: var(--bg-deep); border: 1px solid var(--border); |
| border-radius: var(--radius); padding: 10px 12px; font-size: 12px; |
| line-height: 1.5; white-space: pre-wrap; word-break: break-word; |
| min-height: 40px; max-height: 280px; overflow-y: auto; |
| } |
| #console-stdout { color: var(--success); } |
| #console-stderr { color: var(--red); } |
| |
| |
| #pane-code { padding: 0; } |
| |
| .code-tab-header { |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 8px 12px; border-bottom: 1px solid var(--border); |
| background: rgba(30,42,58,0.3); flex-shrink: 0; |
| } |
| .code-tab-lang { |
| font-size: 11px; color: var(--amber); letter-spacing: 1px; text-transform: uppercase; |
| } |
| .code-tab-actions { display: flex; gap: 8px; } |
| .code-tab-btn { |
| background: transparent; border: 1px solid var(--border); color: var(--gray-mid); |
| font-family: var(--font-mono); font-size: 10px; padding: 3px 8px; |
| border-radius: 3px; cursor: pointer; text-decoration: none; |
| transition: all var(--transition); display: inline-flex; |
| align-items: center; gap: 4px; |
| } |
| .code-tab-btn:hover { border-color: var(--cyan); color: var(--cyan); text-decoration: none; } |
| |
| #code-display { |
| flex: 1; overflow: auto; padding: 12px; background: var(--bg-code); |
| } |
| #code-display pre { margin: 0; font-size: 12px; line-height: 1.5; color: var(--code-text); } |
| |
| .code-placeholder { |
| display: flex; align-items: center; justify-content: center; |
| height: 100%; color: var(--gray-dim); font-size: 12px; |
| } |
| |
| |
| |
| |
| #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-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; |
| } |
| |
| |
| |
| |
| #pane-deploy { padding: 16px; gap: 14px; overflow-y: auto; } |
| |
| .deploy-section { |
| border: 1px solid var(--border); |
| border-radius: var(--radius); |
| padding: 14px; |
| background: var(--bg-code); |
| } |
| |
| .deploy-title { |
| font-size: 12px; |
| font-weight: 600; |
| color: var(--purple); |
| text-shadow: var(--glow-purple); |
| margin-bottom: 10px; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| } |
| |
| .deploy-field { |
| margin-bottom: 10px; |
| } |
| |
| .deploy-field label { |
| display: block; |
| font-size: 10px; |
| color: var(--gray-dim); |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| margin-bottom: 4px; |
| } |
| |
| .deploy-field input, .deploy-field select { |
| width: 100%; |
| background: var(--bg-deep); |
| border: 1px solid var(--border); |
| color: var(--green); |
| font-family: var(--font-mono); |
| font-size: 12px; |
| padding: 6px 10px; |
| border-radius: var(--radius); |
| outline: none; |
| transition: border-color var(--transition); |
| } |
| .deploy-field input:focus, .deploy-field select:focus { |
| border-color: var(--border-focus); |
| } |
| .deploy-field input::placeholder { |
| color: var(--gray-dim); |
| } |
| .deploy-field select option { |
| background: var(--bg-deep); |
| color: var(--gray-light); |
| } |
| |
| .deploy-hint { |
| font-size: 10px; |
| color: var(--gray-dim); |
| margin-top: 3px; |
| } |
| |
| #btn-push-hf { |
| width: 100%; |
| background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(57,255,20,0.1)); |
| border: 1px solid var(--purple); |
| color: var(--purple); |
| font-family: var(--font-mono); |
| font-size: 12px; |
| padding: 8px 14px; |
| border-radius: var(--radius); |
| cursor: pointer; |
| transition: all var(--transition); |
| letter-spacing: 1px; |
| margin-top: 6px; |
| } |
| #btn-push-hf:hover:not(:disabled) { |
| background: var(--purple); |
| color: white; |
| box-shadow: 0 0 12px rgba(168,85,247,0.4); |
| text-shadow: none; |
| } |
| #btn-push-hf:disabled { |
| opacity: 0.4; |
| cursor: not-allowed; |
| } |
| |
| .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 { |
| 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 { |
| display: flex; align-items: center; gap: 8px; padding: 5px 16px; |
| border-top: 1px solid var(--border); background: var(--bg-panel); |
| font-size: 11px; flex-shrink: 0; |
| } |
| |
| .status-indicator { |
| display: inline-flex; align-items: center; gap: 6px; |
| } |
| .status-dot { font-size: 10px; line-height: 1; } |
| #status-text { letter-spacing: 1px; text-transform: uppercase; } |
| |
| .status-idle { color: var(--gray-dim); } |
| .status-working { color: var(--amber); text-shadow: var(--glow-amber); } |
| .status-success { color: var(--success); text-shadow: 0 0 8px rgba(80,250,123,0.3); } |
| .status-error { color: var(--red); text-shadow: 0 0 8px rgba(255,85,85,0.3); } |
| .status-info { color: var(--cyan); text-shadow: var(--glow-cyan); } |
| |
| @keyframes spin { to { transform: rotate(360deg); } } |
| .status-working .status-dot { display: inline-block; animation: spin 1s linear infinite; } |
| |
| |
| |
| |
| #fullscreen-overlay { |
| display: none; position: fixed; inset: 0; z-index: 1000; |
| background: var(--bg-deep); flex-direction: column; |
| } |
| #fullscreen-overlay.active { display: flex; } |
| |
| #fullscreen-bar { |
| display: flex; align-items: center; justify-content: space-between; |
| padding: 8px 16px; border-bottom: 1px solid var(--border); background: var(--bg-panel); |
| } |
| #fullscreen-bar span { color: var(--cyan); font-size: 12px; letter-spacing: 1px; } |
| #btn-exit-fullscreen { |
| background: transparent; border: 1px solid var(--border); color: var(--gray-mid); |
| font-family: var(--font-mono); font-size: 11px; padding: 4px 12px; |
| border-radius: var(--radius); cursor: pointer; transition: all var(--transition); |
| } |
| #btn-exit-fullscreen:hover { border-color: var(--red); color: var(--red); } |
| #fullscreen-iframe { flex: 1; border: none; background: #fff; } |
| |
| |
| |
| |
| @media (max-width: 900px) { |
| #main { flex-direction: column; } |
| #terminal-panel { border-right: none; border-bottom: 1px solid var(--border); max-height: 55vh; } |
| #output-panel { width: 100%; max-width: 100%; min-width: 0; flex: 1; } |
| .header-ascii { font-size: 10px; } |
| #chat-input { font-size: 12px; } |
| #preview-iframe { min-height: 400px; } |
| } |
| |
| @media (max-width: 600px) { |
| #header { padding: 8px 12px; gap: 8px; } |
| .header-ascii { display: none; } |
| .header-subtitle { display: none; } |
| .pill { font-size: 10px; padding: 3px 8px; } |
| #chat-messages { padding: 10px; } |
| #input-area { padding: 8px 10px 6px; } |
| #examples-row { display: none; } |
| #target-selector { gap: 6px; } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="app"> |
| |
| <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> |
|
|
| |
| <div id="main"> |
| |
| <div id="terminal-panel"> |
| <div class="panel-label">Terminal</div> |
| <div id="chat-messages"></div> |
| <div id="input-area"> |
| <div id="target-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> |
|
|
| |
| <div id="output-panel"> |
| <div id="output-tabs"> |
| <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button> |
| <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button> |
| <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button> |
| <button class="output-tab" data-tab="search" onclick="switchTab('search')">Search</button> |
| <button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</button> |
| </div> |
| <div id="output-content"> |
| |
| <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> |
|
|
| |
| <div class="tab-pane" id="pane-console"> |
| <div class="console-section"> |
| <div class="console-label">stdout:</div> |
| <div class="console-output" id="console-stdout">No output yet.</div> |
| </div> |
| <div class="console-section"> |
| <div class="console-label">stderr:</div> |
| <div class="console-output" id="console-stderr">No errors.</div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-pane" id="pane-code"> |
| <div class="code-tab-header"> |
| <span class="code-tab-lang" id="code-tab-lang">—</span> |
| <div class="code-tab-actions"> |
| <button class="code-tab-btn" id="btn-copy-code" onclick="copyCode()">📋 Copy</button> |
| <a class="code-tab-btn" id="btn-download" href="#" style="display:none;">⬇ Download</a> |
| </div> |
| </div> |
| <div id="code-display"> |
| <div class="code-placeholder">No code generated yet.</div> |
| </div> |
| </div> |
|
|
| |
| <div 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> |
|
|
| |
| <div class="tab-pane" id="pane-deploy"> |
| <div class="deploy-section"> |
| <div class="deploy-title">🚀 Deploy to HuggingFace</div> |
| <div class="deploy-field"> |
| <label for="hf-repo-name">Repository Name</label> |
| <input type="text" id="hf-repo-name" placeholder="username/my-app" autocomplete="off"> |
| <div class="deploy-hint">Format: username/repo-name or just repo-name</div> |
| </div> |
| <div class="deploy-field"> |
| <label for="hf-token">HuggingFace Token</label> |
| <input type="password" id="hf-token" placeholder="hf_xxxxxxxxxxxxxxxxxxxxx" autocomplete="off"> |
| <div class="deploy-hint">Get your token at <a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</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> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="status-bar"> |
| <div class="status-indicator status-idle" id="status-indicator"> |
| <span class="status-dot">●</span> |
| <span id="status-text">LOADING MODEL...</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="fullscreen-overlay"> |
| <div id="fullscreen-bar"> |
| <span>WEB PREVIEW</span> |
| <button id="btn-exit-fullscreen" onclick="closeFullscreen()">[✕ CLOSE]</button> |
| </div> |
| <iframe id="fullscreen-iframe" sandbox="allow-scripts"></iframe> |
| </div> |
|
|
| <script> |
| |
| |
| |
| const CONFIG = __RUNTIME_CONFIG__; |
| |
| |
| |
| |
| const state = { |
| history: [], |
| executionContext: {}, |
| targetLanguage: 'Python', |
| 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: '', |
| }; |
| |
| |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| document.title = CONFIG.app_title || 'Fullstack Code Builder'; |
| |
| 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); } |
| |
| |
| populateLanguageSelects(); |
| |
| |
| renderExamples(); |
| |
| |
| addSystemMessage('Welcome to Fullstack Code Builder. The model is loading locally (no API keys needed). Select a language and framework, then describe the app you want to build.'); |
| |
| |
| const input = document.getElementById('chat-input'); |
| input.addEventListener('input', autoResize); |
| input.addEventListener('keydown', (e) => { |
| if (e.key === 'Enter' && e.shiftKey) { |
| e.preventDefault(); |
| handleSend(); |
| } |
| }); |
| |
| |
| 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(); |
| |
| |
| pollModelStatus(); |
| }); |
| |
| function autoResize() { |
| const el = document.getElementById('chat-input'); |
| el.style.height = 'auto'; |
| el.style.height = Math.min(el.scrollHeight, 120) + 'px'; |
| } |
| |
| |
| |
| |
| 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; |
| |
| 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; |
| } |
| |
| const imageGroup = document.getElementById('image-attach-group'); |
| if (state.currentModelType === 'vlm') { |
| imageGroup.style.display = 'flex'; |
| } else { |
| imageGroup.style.display = 'none'; |
| } |
| |
| 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); |
| } |
| } |
| |
| |
| |
| |
| function populateLanguageSelects() { |
| const langSelect = document.getElementById('lang-select'); |
| const fwSelect = document.getElementById('framework-select'); |
| if (!langSelect || !fwSelect) return; |
| |
| |
| 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; |
| |
| |
| 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); |
| }; |
| |
| |
| 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)) { |
| |
| 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'; |
| } |
| } |
| |
| |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| |
| |
| 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; }); |
| } |
| |
| |
| |
| |
| 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; |
| } |
| |
| |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| |
| |
| 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'; |
| } |
| |
| |
| |
| |
| 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; |
| |
| |
| document.getElementById('console-stdout').textContent = execution.stdout || 'No output.'; |
| document.getElementById('console-stderr').textContent = execution.stderr || 'No errors.'; |
| |
| |
| if (execution.code) { |
| state.lastCode = execution.code; |
| state.lastCodeLang = execution.language || 'code'; |
| document.getElementById('code-tab-lang').textContent = state.lastCodeLang; |
| document.getElementById('code-display').innerHTML = `<pre>${escapeHtml(execution.code)}</pre>`; |
| } |
| |
| |
| const dlBtn = document.getElementById('btn-download'); |
| if (execution.download_url) { |
| dlBtn.href = execution.download_url; |
| dlBtn.style.display = 'inline-flex'; |
| dlBtn.setAttribute('download', ''); |
| } else { |
| dlBtn.style.display = 'none'; |
| } |
| |
| |
| const placeholder = document.getElementById('preview-placeholder'); |
| const img = document.getElementById('preview-image'); |
| const iframe = 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) { |
| |
| placeholder.style.display = 'none'; |
| img.style.display = 'none'; |
| iframe.style.display = 'block'; |
| fsBtn.style.display = 'block'; |
| |
| 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); |
| } |
| } |
| |
| |
| renderProjectFiles(execution.project_files || {}); |
| |
| |
| 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 = ''; |
| } |
| |
| |
| |
| |
| 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(); |
| } |
| |
| |
| |
| |
| 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'); |
| |
| |
| if (payload.type === 'search_results' && payload.search_results) { |
| state.currentSearchResults = payload.search_results; |
| state.lastSearchResults = payload.search_results; |
| renderSearchSourceBadge(payload.search_results, false); |
| |
| renderSearchResults(payload.search_results); |
| } |
| |
| if (payload.history) { |
| state.history = payload.history; |
| const lastMsg = payload.history[payload.history.length - 1]; |
| if (lastMsg && lastMsg.role === 'assistant') { |
| updateAssistantMessage(lastMsg.content, isStreaming); |
| } |
| } |
| |
| if (payload.execution) { |
| renderExecution(payload.execution); |
| if (payload.execution) state.executionContext = payload.execution; |
| } |
| |
| if (payload.type === 'complete') { |
| finalizeAssistantMessage(); |
| renderStatus('Done', 'success'); |
| setTimeout(() => { if (!state.isGenerating) renderStatus('Idle', 'idle'); }, 3000); |
| } |
| |
| if (payload.type === 'error') { |
| finalizeAssistantMessage(); |
| addSystemMessage(`Error: ${payload.status_text || 'Unknown error'}`); |
| renderStatus('Error', 'error'); |
| } |
| |
| if (payload.execution && payload.execution.suggested_tab) { |
| switchTab(payload.execution.suggested_tab); |
| } |
| } |
| |
| function onGenerationEnd() { |
| state.isGenerating = false; |
| state.currentEventSource = null; |
| toggleInputState(false); |
| } |
| |
| function toggleInputState(generating) { |
| const sendBtn = document.getElementById('btn-send'); |
| const stopBtn = document.getElementById('btn-stop'); |
| const input = document.getElementById('chat-input'); |
| |
| if (generating) { |
| sendBtn.style.display = 'none'; |
| stopBtn.style.display = 'flex'; |
| input.disabled = true; |
| input.placeholder = 'Generating...'; |
| } else { |
| sendBtn.style.display = 'flex'; |
| stopBtn.style.display = 'none'; |
| sendBtn.disabled = false; |
| input.disabled = false; |
| input.placeholder = 'Describe the app you want to build...'; |
| input.focus(); |
| } |
| } |
| |
| function stopGeneration() { |
| if (state.currentEventSource) { |
| state.currentEventSource.close(); |
| state.currentEventSource = null; |
| } |
| finalizeAssistantMessage(); |
| addSystemMessage('Generation stopped by user.'); |
| renderStatus('Stopped', 'info'); |
| onGenerationEnd(); |
| } |
| |
| function resetConversation(announcement) { |
| state.history = []; |
| state.executionContext = {}; |
| state.lastExecution = null; |
| state.lastCode = ''; |
| state.lastCodeLang = ''; |
| state.reasoningExpanded = false; |
| |
| if (state.currentEventSource) { |
| state.currentEventSource.close(); |
| state.currentEventSource = null; |
| } |
| state.isGenerating = false; |
| toggleInputState(false); |
| |
| document.getElementById('chat-messages').innerHTML = ''; |
| resetOutput(); |
| switchTab('preview'); |
| renderStatus('Idle', 'idle'); |
| if (announcement) addSystemMessage(announcement); |
| } |
| |
| function newChat() { |
| resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'Fullstack Code Builder'}.`); |
| } |
| |
| 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'; |
| } |
| } |
| |
| |
| |
| |
| 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...`); |
| |
| 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); |
| } |
| |
| |
| |
| |
| 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'; |
| |
| |
| const reader = new FileReader(); |
| reader.onload = async function(e) { |
| const base64Data = e.target.result; |
| |
| |
| 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'; |
| } |
| |
| |
| |
| |
| 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); |
| |
| 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; |
| } |
| |
| |
| |
| |
| 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' : ''; |
| |
| |
| 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(); |
| } |
| |
| |
| |
| |
| async function pushToHuggingFace() { |
| 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'); |
| |
| if (!repoName) { |
| 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 enter your HuggingFace 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, repoName, hfToken, spaceSdk, 'true'] |
| }) |
| }); |
| |
| if (!resp.ok) throw new Error(`API error: ${resp.status}`); |
| |
| const { event_id } = await resp.json(); |
| |
| const eventSource = new EventSource(`/gradio_api/call/push_hf/${event_id}`); |
| |
| eventSource.addEventListener('complete', (e) => { |
| try { |
| const dataArray = JSON.parse(e.data); |
| const result = JSON.parse(dataArray[0]); |
| |
| if (result.success) { |
| statusEl.className = 'deploy-status success'; |
| statusEl.innerHTML = `\u2713 ${result.message}<br><a href="${result.url}" target="_blank" rel="noopener">${result.url} \u2197</a>`; |
| } else { |
| statusEl.className = 'deploy-status error'; |
| statusEl.textContent = `\u2717 ${result.message}`; |
| } |
| } catch (err) { |
| statusEl.className = 'deploy-status error'; |
| statusEl.textContent = `Parse error: ${err.message}`; |
| } |
| eventSource.close(); |
| btn.disabled = false; |
| }); |
| |
| eventSource.addEventListener('error', (e) => { |
| statusEl.className = 'deploy-status error'; |
| statusEl.textContent = `Push failed: ${e.data || 'Unknown error'}`; |
| eventSource.close(); |
| btn.disabled = false; |
| }); |
| |
| } catch (err) { |
| statusEl.className = 'deploy-status error'; |
| statusEl.textContent = `Push failed: ${err.message}`; |
| btn.disabled = false; |
| } |
| } |
| </script> |
| </body> |
| </html> |
|
|