Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no"/> | |
| <title>BuildAI β Builder</title> | |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>β‘</text></svg>"/> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/> | |
| <style> | |
| *,*::before,*::after{margin:0;padding:0;box-sizing:border-box} | |
| /* ββ ROOT LAYOUT β works inside HuggingFace iframe ββ */ | |
| :root{ | |
| --bg:#07080f;--bg2:#0b0d17;--bg3:#0f1220;--bg4:#090b12; | |
| --border:#0f1220;--border2:#1a1f33; | |
| --text:#e2e8f0;--muted:#475569;--dim:#334155;--faint:#1e2740; | |
| --accent:#6366f1;--accent2:#8b5cf6;--green:#10b981; | |
| --toolbar-h:46px;--mobtab-h:44px; | |
| } | |
| html{height:100%;height:100dvh;overflow:hidden} | |
| body{ | |
| height:100%;height:100dvh; | |
| display:flex;flex-direction:column; | |
| font-family:'Inter',system-ui,sans-serif; | |
| background:var(--bg);color:var(--text); | |
| -webkit-font-smoothing:antialiased; | |
| overflow:hidden; | |
| } | |
| button,input,textarea{font-family:inherit} | |
| ::-webkit-scrollbar{width:4px;height:4px} | |
| ::-webkit-scrollbar-track{background:transparent} | |
| ::-webkit-scrollbar-thumb{background:#1e2740;border-radius:99px} | |
| /* ββ ANIMATIONS ββ */ | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| @keyframes fadeUp{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:translateY(0)}} | |
| @keyframes fadeIn{from{opacity:0}to{opacity:1}} | |
| @keyframes floatY{0%,100%{transform:translateY(0)}50%{transform:translateY(-9px)}} | |
| @keyframes blink{0%,100%{opacity:.2}50%{opacity:1}} | |
| @keyframes dotBounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}} | |
| @keyframes slideDown{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} | |
| @keyframes gradFlow{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}} | |
| /* ββ TOOLBAR ββ */ | |
| .tb{ | |
| height:var(--toolbar-h); | |
| display:flex;align-items:center; | |
| padding:0 10px;gap:4px; | |
| border-bottom:1px solid var(--border); | |
| background:var(--bg2); | |
| flex-shrink:0; | |
| z-index:200; | |
| overflow:visible; | |
| position:relative; | |
| } | |
| .tb-left{display:flex;align-items:center;gap:4px;flex-shrink:0} | |
| .tb-logo{display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 8px;border-radius:7px;transition:background .15s;text-decoration:none;color:inherit} | |
| .tb-logo:hover{background:#13172a} | |
| .tb-lm{width:20px;height:20px;background:linear-gradient(135deg,var(--accent),var(--accent2));border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0} | |
| .tb-ln{font-size:13px;font-weight:800;letter-spacing:-.3px} | |
| .tb-proj{display:flex;align-items:center;gap:5px;padding:4px 9px;background:#13172a;border:1px solid var(--border2);border-radius:7px;cursor:pointer;max-width:140px;flex-shrink:0} | |
| .tb-pn{font-size:11px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .tb-sep{width:1px;height:22px;background:var(--border);margin:0 3px;flex-shrink:0} | |
| .tb-ic{height:28px;background:transparent;border:none;border-radius:6px;color:var(--dim);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;transition:background .15s,color .15s;padding:0 8px;gap:4px;white-space:nowrap} | |
| .tb-ic:hover{background:#13172a;color:var(--muted)} | |
| .tb-center{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0} | |
| .tab-b{padding:5px 12px;background:transparent;border:1px solid transparent;border-radius:7px;color:var(--dim);font-size:11px;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s} | |
| .tab-b.on{background:#13172a;border-color:var(--border2);color:#c4d4f0;font-weight:600} | |
| .tab-b:hover:not(.on){color:var(--muted)} | |
| .tb-url{display:flex;align-items:center;gap:5px;padding:4px 10px;background:var(--bg3);border:1px solid #13172a;border-radius:7px;margin-left:6px;max-width:180px;overflow:hidden} | |
| .tb-url-t{font-size:10px;color:var(--dim);font-family:'JetBrains Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .tb-right{display:flex;align-items:center;gap:4px;flex-shrink:0} | |
| .vp-grp{display:flex;gap:1px;background:var(--bg3);border:1px solid #13172a;border-radius:7px;padding:2px;flex-shrink:0} | |
| .vp-b{width:26px;height:23px;background:transparent;border:none;border-radius:5px;cursor:pointer;font-size:11px;transition:background .15s;display:flex;align-items:center;justify-content:center} | |
| .vp-b.on{background:#1e2740} | |
| .tb-tool{padding:5px 10px;background:#13172a;border:1px solid var(--border2);border-radius:7px;color:var(--muted);font-size:11px;font-weight:500;cursor:pointer;display:flex;align-items:center;gap:4px;white-space:nowrap;flex-shrink:0;transition:all .15s} | |
| .tb-tool:hover{border-color:#2e3a55;color:#94a3b8} | |
| .tb-pub{padding:5px 13px;background:linear-gradient(135deg,#059669,var(--green));border:none;border-radius:7px;color:#fff;font-size:11px;font-weight:700;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .2s} | |
| .tb-pub:hover:not(:disabled){opacity:.9} | |
| .tb-pub:disabled{opacity:.4;cursor:default} | |
| .tb-signin{padding:5px 13px;background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;border-radius:7px;color:#fff;font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;flex-shrink:0} | |
| .tb-guest{padding:5px 11px;background:#13172a;border:1px solid var(--border2);border-radius:7px;color:var(--muted);font-size:11px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .15s} | |
| .tb-guest:hover{border-color:var(--accent);color:#818cf8} | |
| .tb-av{width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;cursor:pointer;flex-shrink:0;position:relative} | |
| .user-dd{position:absolute;top:34px;right:0;width:210px;background:#0e1018;border:1px solid var(--border2);border-radius:12px;z-index:9999;animation:slideDown .15s ease;box-shadow:0 20px 50px rgba(0,0,0,.7);overflow:hidden} | |
| .ud-head{padding:12px 14px;border-bottom:1px solid var(--border)} | |
| .ud-name{font-size:13px;font-weight:600;color:#c4d4f0;margin-bottom:2px} | |
| .ud-email{font-size:11px;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .ud-item{padding:10px 14px;font-size:12px;color:var(--muted);cursor:pointer;display:flex;align-items:center;gap:8px;transition:background .15s} | |
| .ud-item:hover{background:var(--bg3);color:#94a3b8} | |
| .ud-item.red:hover{color:#f87171} | |
| .ud-sep{height:1px;background:var(--border)} | |
| /* ββ MOBILE TABS ββ */ | |
| .mob-tabs{ | |
| display:none;height:var(--mobtab-h); | |
| background:var(--bg2);border-bottom:1px solid var(--border); | |
| flex-shrink:0; | |
| } | |
| .mob-tab{ | |
| flex:1;display:flex;align-items:center;justify-content:center;gap:5px; | |
| background:transparent;border:none;border-bottom:2px solid transparent; | |
| color:var(--dim);font-size:12px;font-weight:500;cursor:pointer;transition:all .15s; | |
| } | |
| .mob-tab.on{color:#c4d4f0;border-bottom-color:var(--accent)} | |
| /* ββ MAIN AREA β critical height containment ββ */ | |
| .main{ | |
| flex:1; | |
| display:flex; | |
| overflow:hidden; | |
| min-height:0; /* CRITICAL: lets flex children shrink below content size */ | |
| } | |
| /* ββ SIDEBAR ββ */ | |
| .sb{ | |
| width:185px;flex-shrink:0; | |
| display:flex;flex-direction:column; | |
| background:var(--bg2);border-right:1px solid var(--border); | |
| overflow:hidden;min-height:0; | |
| } | |
| .sb-hd{padding:8px 10px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0} | |
| .sb-lbl{font-size:10px;font-weight:700;color:var(--faint);text-transform:uppercase;letter-spacing:1.2px} | |
| .sb-new{ | |
| width:calc(100% - 14px);margin:7px;padding:9px; | |
| background:var(--bg3);border:1px dashed var(--border2);border-radius:8px; | |
| color:var(--dim);font-size:11px;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center;gap:5px; | |
| transition:all .2s;flex-shrink:0; | |
| } | |
| .sb-new:hover{border-color:var(--accent);color:var(--accent)} | |
| .sb-list{flex:1;overflow-y:auto;padding:0 7px 8px;min-height:0} | |
| .sb-sec{font-size:9.5px;font-weight:700;color:#131828;text-transform:uppercase;letter-spacing:1.2px;padding:6px 8px 4px} | |
| .sb-empty{padding:20px 10px;text-align:center;font-size:11px;color:#131828;line-height:1.8} | |
| .sb-signin-prompt{margin:10px 7px;padding:12px;background:rgba(99,102,241,.06);border:1px solid rgba(99,102,241,.15);border-radius:10px;text-align:center} | |
| .sb-sp-t{font-size:11px;color:var(--dim);margin-bottom:8px;line-height:1.6} | |
| .sb-sp-b{width:100%;padding:7px;background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;border-radius:7px;color:#fff;font-size:11px;font-weight:600;cursor:pointer} | |
| .pi{display:flex;align-items:center;gap:8px;padding:8px 9px;border-radius:8px;cursor:pointer;margin-bottom:2px;border:1px solid transparent;transition:all .15s} | |
| .pi:hover{background:var(--bg3)} | |
| .pi.on{background:#111e36;border-color:#1e3a5f} | |
| .pi-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0} | |
| .pi-n{font-size:11.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .pi.on .pi-n{color:#c4d4f0;font-weight:500} | |
| .pi-d{font-size:9.5px;color:#131828;margin-top:1px} | |
| .sb-ft{padding:8px;border-top:1px solid var(--border);flex-shrink:0} | |
| .sb-user{display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--bg3);border-radius:8px;cursor:pointer;transition:background .15s} | |
| .sb-user:hover{background:#13172a} | |
| .sb-uav{width:22px;height:22px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0} | |
| .sb-un{font-size:11px;font-weight:600;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .sb-up{font-size:9.5px;color:var(--faint)} | |
| .sb-signout{font-size:10px;color:var(--dim);background:none;border:none;cursor:pointer;margin-top:4px;display:none;width:100%;text-align:center;padding:4px;transition:color .15s} | |
| .sb-signout:hover{color:#f87171} | |
| /* ββ CHAT PANEL ββ */ | |
| .cp{ | |
| width:290px;flex-shrink:0; | |
| display:flex;flex-direction:column; | |
| border-right:1px solid var(--border); | |
| background:var(--bg2); | |
| overflow:hidden;min-height:0; | |
| } | |
| .c-stat{padding:7px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:7px;background:#09091a;flex-shrink:0} | |
| .r-dots{display:flex;gap:3px} | |
| .r-dot{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:7px;font-weight:700;border:1px solid var(--faint);color:var(--faint);transition:all .35s;flex-shrink:0} | |
| .r-dot.act{border-color:var(--accent2);background:rgba(139,92,246,.2);color:#c4b5fd} | |
| .r-dot.dn{border-color:var(--green);background:rgba(16,185,129,.15);color:#34d399} | |
| .c-stat-t{font-size:11px;color:var(--dim);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .c-msgs{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:10px;min-height:0} | |
| .c-empty{display:flex;flex-direction:column;align-items:center;padding:28px 16px;text-align:center} | |
| .c-e-ic{font-size:40px;margin-bottom:12px;animation:floatY 3.5s ease-in-out infinite} | |
| .c-e-t{font-size:14px;font-weight:700;color:#c4d4f0;margin-bottom:6px} | |
| .c-e-s{font-size:12px;color:var(--dim);line-height:1.7;margin-bottom:16px} | |
| .q-prompts{display:flex;flex-direction:column;gap:7px;width:100%} | |
| .q-p{padding:9px 12px;background:var(--bg3);border:1px solid var(--border2);border-radius:9px;color:var(--muted);font-size:11.5px;cursor:pointer;text-align:left;transition:all .2s} | |
| .q-p:hover{border-color:rgba(99,102,241,.35);color:#94a3b8;background:rgba(99,102,241,.06)} | |
| .m-u{display:flex;justify-content:flex-end} | |
| .m-u-b{background:#1a1f33;border-radius:13px 13px 3px 13px;padding:9px 13px;font-size:12.5px;color:#c4d4f0;max-width:87%;line-height:1.6} | |
| .m-a{display:flex;flex-direction:column;gap:7px} | |
| .m-a-lbl{font-size:9px;font-weight:700;color:var(--faint);letter-spacing:1.5px} | |
| .m-card{background:var(--bg3);border:1px solid var(--border2);border-radius:10px;padding:10px 12px;display:flex;align-items:center;justify-content:space-between;gap:8px} | |
| .m-card-h{font-size:12px;font-weight:600;color:#c4d4f0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .m-card-tabs{display:flex;gap:4px;flex-shrink:0} | |
| .m-card-tb{padding:4px 9px;background:transparent;border:1px solid var(--faint);border-radius:6px;color:var(--dim);font-size:10px;cursor:pointer;transition:all .15s} | |
| .m-card-tb.on{background:var(--faint);color:#c4d4f0;border-color:#2e3a55} | |
| .m-card-tb:hover:not(.on){color:var(--muted)} | |
| .m-think{background:#0a0f1e;border:1px solid var(--border2);border-radius:10px;padding:12px 14px;font-size:11.5px;line-height:1.9} | |
| .think-hd{display:flex;align-items:center;gap:8px;font-weight:600;color:var(--accent);margin-bottom:8px} | |
| .think-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);flex-shrink:0;animation:blink 1.2s ease-in-out infinite} | |
| .think-ln{color:var(--dim)} | |
| .think-ln.ap{color:var(--muted);margin-bottom:6px} | |
| .think-ln.bl{padding-left:8px} | |
| .think-ln.dn{color:var(--green)} | |
| .m-txt{font-size:12px;color:var(--muted);line-height:1.7} | |
| .m-save-note{font-size:11px;color:var(--green);display:flex;align-items:center;gap:5px} | |
| .m-next-t{font-size:11px;font-weight:700;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-top:4px} | |
| .m-next-i{font-size:11.5px;color:var(--muted);line-height:1.7} | |
| .m-acts{display:flex;gap:6px;flex-wrap:wrap} | |
| .m-act{padding:6px 11px;background:var(--bg3);border:1px solid var(--border2);border-radius:7px;color:var(--muted);font-size:11px;cursor:pointer;transition:all .15s;white-space:nowrap} | |
| .m-act:hover{border-color:rgba(99,102,241,.3);color:#818cf8} | |
| .m-act.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));border-color:transparent;color:#fff;font-weight:600} | |
| .m-act.primary:hover{opacity:.9} | |
| .m-err{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.15);border-radius:9px;padding:10px 13px;font-size:12px;color:#f87171;line-height:1.6} | |
| .typing{display:flex;gap:4px;padding:4px 0} | |
| .td{width:5px;height:5px;border-radius:50%;background:var(--faint);animation:dotBounce .8s ease-in-out infinite} | |
| .td:nth-child(2){animation-delay:.15s} | |
| .td:nth-child(3){animation-delay:.3s} | |
| .sugg-bar{padding:8px 12px;border-top:1px solid var(--border);display:flex;gap:6px;overflow-x:auto;flex-shrink:0;scrollbar-width:none} | |
| .sugg-bar::-webkit-scrollbar{display:none} | |
| .s-chip{padding:5px 10px;background:var(--bg3);border:1px solid var(--border2);border-radius:999px;color:var(--muted);font-size:11px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .15s} | |
| .s-chip:hover{border-color:rgba(99,102,241,.3);color:#818cf8} | |
| .c-inp-area{padding:8px;border-top:1px solid var(--border);flex-shrink:0;background:#09091a} | |
| .c-inp-row{display:flex;align-items:flex-end;gap:7px;background:rgba(255,255,255,.03);border:1px solid var(--border2);border-radius:13px;padding:7px 7px 7px 13px;transition:border-color .2s} | |
| .c-inp-row:focus-within{border-color:rgba(99,102,241,.35)} | |
| .c-ta{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-size:13px;resize:none;min-height:24px;max-height:80px;line-height:1.6;padding:2px 0} | |
| .c-ta::placeholder{color:var(--faint)} | |
| .c-ta:disabled{opacity:.5;cursor:not-allowed} | |
| .c-send{width:34px;height:34px;border-radius:9px;border:none;flex-shrink:0;background:#1a1f33;color:var(--dim);cursor:default;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .2s} | |
| .c-send.on{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;cursor:pointer} | |
| .c-send.on:hover{opacity:.9} | |
| /* ββ PREVIEW PANEL β critical layout fix ββ */ | |
| .pv{ | |
| flex:1; | |
| display:flex;flex-direction:column; | |
| overflow:hidden;min-width:0;min-height:0; /* CRITICAL */ | |
| } | |
| .pub-bar{padding:5px 14px;background:var(--bg2);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-shrink:0} | |
| .pub-dot{width:5px;height:5px;border-radius:50%;background:var(--green);flex-shrink:0;animation:pulse 2s ease-in-out infinite} | |
| .pub-t{font-size:10px;color:var(--green)} | |
| .pub-dt{font-size:10px;color:#131828;margin-left:auto} | |
| .pv-body{ | |
| flex:1; | |
| position:relative; | |
| overflow:hidden; /* contain the iframe/code */ | |
| display:flex;align-items:center;justify-content:center; | |
| background:#10121e; | |
| min-height:0; /* CRITICAL */ | |
| } | |
| .pv-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;height:100%;width:100%} | |
| .pv-e-ic{font-size:64px;opacity:.09;animation:floatY 3.5s ease-in-out infinite} | |
| .pv-e-t{font-size:15px;font-weight:600;color:#131828} | |
| .pv-overlay{position:absolute;inset:0;background:rgba(7,8,15,.93);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;backdrop-filter:blur(10px)} | |
| .ov-spin{width:48px;height:48px;border-radius:50%;border:3px solid #1a1f33;border-top:3px solid var(--accent);animation:spin .85s linear infinite} | |
| .ov-t{font-size:13px;color:var(--muted);text-align:center;max-width:240px;line-height:1.6} | |
| .ov-s{font-size:11px;color:#131828;text-align:center} | |
| .ov-badges{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:4px} | |
| .ov-b{padding:3px 9px;background:rgba(99,102,241,.1);border:1px solid rgba(99,102,241,.2);border-radius:999px;font-size:10px;color:#a78bfa;font-weight:600} | |
| .ov-b.act{background:rgba(99,102,241,.25);border-color:rgba(99,102,241,.5);color:#c4b5fd} | |
| .ov-bar{width:200px;height:2px;background:#1a1f33;border-radius:999px;overflow:hidden;margin-top:6px} | |
| .ov-prog{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:999px;transition:width .6s ease} | |
| /* Code view β must be contained */ | |
| .code-wrap{width:100%;height:100%;overflow:auto;background:var(--bg)} | |
| .code-pre{margin:0;padding:20px;font-size:11px;line-height:1.9;color:#5a6a8a;font-family:'JetBrains Mono',monospace;white-space:pre-wrap;word-break:break-all} | |
| /* ββ File Tree ββ */ | |
| .file-tree{width:160px;min-width:140px;background:#0a0c18;border-right:1px solid var(--border);overflow-y:auto;flex-shrink:0;display:flex;flex-direction:column} | |
| .file-tree-h{padding:8px 10px;font-size:10px;font-weight:700;color:var(--faint);letter-spacing:.08em;text-transform:uppercase;border-bottom:1px solid var(--border)} | |
| .file-item{padding:6px 10px;font-size:10px;font-family:'JetBrains Mono',monospace;color:var(--dim);cursor:pointer;border-left:2px solid transparent;transition:all .12s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .file-item:hover{color:var(--text);background:rgba(99,102,241,.08)} | |
| .file-item.on{color:#a78bfa;border-left-color:#a78bfa;background:rgba(167,139,250,.08)} | |
| .file-item.generating{color:#f59e0b;animation:pulse 1s infinite} | |
| .file-item.done{color:#10b981} | |
| .file-ext-ts{color:#3b82f6}.file-ext-tsx{color:#38bdf8}.file-ext-css{color:#f472b6}.file-ext-js{color:#fbbf24}.file-ext-html{color:#34d399}.file-ext-py{color:#a78bfa} | |
| .code-view{display:flex;height:100%;overflow:hidden} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} | |
| .pv-btm{padding:7px 14px;border-top:1px solid var(--border);background:var(--bg2);display:flex;align-items:center;gap:7px;flex-shrink:0;flex-wrap:wrap} | |
| .dl-btn{padding:5px 12px;background:var(--bg3);border:1px solid var(--border2);border-radius:7px;color:var(--dim);font-size:11px;cursor:pointer;display:flex;align-items:center;gap:5px;transition:all .15s;white-space:nowrap} | |
| .dl-btn:hover{border-color:rgba(99,102,241,.3);color:var(--accent)} | |
| .dl-zip{padding:5px 12px;background:rgba(99,102,241,.1);border:1px solid rgba(99,102,241,.25);border-radius:7px;color:#a78bfa;font-size:11px;cursor:pointer;display:flex;align-items:center;gap:5px;transition:all .15s;white-space:nowrap;font-weight:600} | |
| .dl-zip:hover{background:rgba(99,102,241,.2);border-color:rgba(99,102,241,.4);color:#c4b5fd} | |
| .pv-hint{font-size:10px;color:var(--border);margin-left:auto} | |
| /* ββ VERSION PANEL ββ */ | |
| .ver-panel{position:absolute;top:34px;left:0;width:250px;background:#0e1018;border:1px solid var(--border2);border-radius:12px;z-index:9999;animation:slideDown .15s ease;box-shadow:0 20px 50px rgba(0,0,0,.7);overflow:hidden} | |
| .ver-hd{padding:10px 14px;border-bottom:1px solid var(--border2);display:flex;justify-content:space-between;align-items:center} | |
| .ver-hd-t{font-size:12px;font-weight:600} | |
| .ver-x{background:none;border:none;color:var(--dim);cursor:pointer;font-size:16px;line-height:1;padding:0 2px} | |
| .ver-x:hover{color:#94a3b8} | |
| .ver-item{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;cursor:pointer;transition:background .15s} | |
| .ver-item:hover{background:var(--bg3)} | |
| .ver-item.cur{background:#0e1928} | |
| .ver-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0} | |
| .ver-n{font-size:12px;color:var(--muted)} | |
| .ver-item.cur .ver-n{color:#c4d4f0;font-weight:500} | |
| .ver-d{font-size:10px;color:var(--faint);margin-top:1px} | |
| .ver-cur-lbl{font-size:10px;color:var(--accent);font-weight:600;margin-left:auto} | |
| .ver-empty{padding:16px 14px;font-size:11px;color:#131828;text-align:center} | |
| /* ββ MODALS ββ */ | |
| .modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:9999;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(12px);padding:16px;animation:fadeIn .15s ease} | |
| .modal-box{background:#0e1018;border:1px solid var(--border2);border-radius:20px;width:100%;max-height:90vh;overflow-y:auto;animation:slideDown .2s ease;box-shadow:0 40px 100px rgba(0,0,0,.7)} | |
| .mo-p{padding:24px} | |
| .mo-hd{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:20px;gap:10px} | |
| .mo-t{font-weight:700;font-size:16px} | |
| .mo-sub{font-size:12px;color:var(--muted);margin-top:2px} | |
| .mo-x{background:none;border:none;color:var(--muted);cursor:pointer;font-size:22px;line-height:1;flex-shrink:0;transition:color .15s;padding:0} | |
| .mo-x:hover{color:#94a3b8} | |
| .flbl{font-size:12px;color:var(--muted);font-weight:500;display:block;margin-bottom:6px} | |
| .finp{width:100%;background:var(--bg4);border:1px solid var(--border2);border-radius:11px;padding:12px 14px;color:var(--text);font-size:13px;outline:none;margin-bottom:12px;display:block;transition:border-color .2s} | |
| .finp:focus{border-color:rgba(99,102,241,.4)} | |
| .finp::placeholder{color:var(--faint)} | |
| .mo-btn-p{width:100%;padding:13px;background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;border-radius:11px;color:#fff;font-size:14px;font-weight:700;cursor:pointer;margin-top:4px;transition:opacity .2s} | |
| .mo-btn-p:hover{opacity:.9} | |
| .mo-btn-p:disabled{opacity:.5;cursor:not-allowed} | |
| .mo-btn-s{width:100%;padding:12px;background:var(--bg4);border:1px solid var(--border2);border-radius:11px;color:var(--muted);font-size:13px;cursor:pointer;margin-top:8px;transition:border-color .2s} | |
| .mo-btn-s:hover{border-color:#2e3a55} | |
| .mo-div{display:flex;align-items:center;gap:12px;margin:16px 0} | |
| .mo-div-l{flex:1;height:1px;background:var(--border2)} | |
| .mo-div-t{font-size:11px;color:var(--faint)} | |
| .tab-sw{display:flex;gap:3px;background:var(--bg4);border-radius:11px;padding:3px;margin-bottom:20px} | |
| .tab-sw-b{flex:1;padding:9px;background:transparent;border:none;border-radius:9px;color:var(--dim);font-size:13px;cursor:pointer;transition:all .15s} | |
| .tab-sw-b.on{background:#1a1f33;color:var(--text);font-weight:600} | |
| .auth-err{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:9px;padding:10px 13px;font-size:12px;color:#f87171;margin-bottom:12px;display:none} | |
| .auth-ok{background:rgba(16,185,129,.08);border:1px solid rgba(16,185,129,.2);border-radius:9px;padding:10px 13px;font-size:12px;color:#34d399;margin-bottom:12px;display:none} | |
| .suc-c{text-align:center;padding:16px 0 8px} | |
| .suc-ic{font-size:52px;margin-bottom:14px;display:block;animation:floatY 2.5s ease-in-out infinite} | |
| .suc-t{font-weight:900;font-size:19px;margin-bottom:8px;letter-spacing:-.4px} | |
| .pub-plan{background:var(--bg4);border:1px solid var(--border2);border-radius:13px;padding:15px 17px;margin-bottom:9px;cursor:pointer;transition:all .2s} | |
| .pub-plan.on{border-color:var(--accent);border-width:2px} | |
| .pub-plan-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;margin-bottom:4px} | |
| .pub-plan-url{font-size:14px;font-weight:700;color:#a78bfa;font-family:'JetBrains Mono',monospace;margin-bottom:4px} | |
| .pub-plan-d{font-size:11px;color:var(--dim)} | |
| .google-btn{width:100%;padding:13px;background:#fff;border:1px solid #e2e8f0;border-radius:11px;color:#1a1a1a;font-size:14px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:10px;transition:all .2s;margin-bottom:4px} | |
| .google-btn:hover{background:#f8fafc;box-shadow:0 4px 16px rgba(0,0,0,.12)} | |
| /* ββ RESPONSIVE ββ */ | |
| @media(max-width:768px){ | |
| .sb{display:none !important} | |
| .mob-tabs{display:flex !important} | |
| .cp{width:100% !important;border-right:none !important} | |
| .pv{width:100% !important} | |
| .tb-tool,.vp-grp,.tb-url{display:none !important} | |
| /* Panel switching via classes on .main */ | |
| .main{flex-direction:row} /* keep row, children handle display */ | |
| .main.show-chat .cp{display:flex !important;flex:1 !important;min-width:0} | |
| .main.show-chat .pv{display:none !important} | |
| .main.show-preview .pv{display:flex !important;flex:1 !important;min-width:0} | |
| .main.show-preview .cp{display:none !important} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ββ TOOLBAR ββ --> | |
| <div class="tb"> | |
| <div class="tb-left"> | |
| <div class="tb-logo" onclick="goHome()"> | |
| <div class="tb-lm">β‘</div> | |
| <span class="tb-ln">BuildAI</span> | |
| </div> | |
| <div class="tb-proj"> | |
| <span class="tb-pn" id="tb-pn">New Project</span> | |
| <span style="font-size:8px;color:var(--dim)">βΎ</span> | |
| </div> | |
| <div class="tb-sep"></div> | |
| <div style="position:relative"> | |
| <button class="tb-ic" id="ver-btn" onclick="toggleVer(event)">π <span style="font-weight:600">History</span></button> | |
| <div class="ver-panel" id="ver-panel" style="display:none"> | |
| <div class="ver-hd"> | |
| <span class="ver-hd-t">Version History</span> | |
| <button class="ver-x" onclick="closeVer()">Γ</button> | |
| </div> | |
| <div id="ver-list"><div class="ver-empty">No versions yet.</div></div> | |
| </div> | |
| </div> | |
| <button class="tb-ic" onclick="undoVer()" title="Undo last version">β©</button> | |
| </div> | |
| <div class="tb-center"> | |
| <button class="tab-b on" id="tab-preview" onclick="switchTab('preview')">π Preview</button> | |
| <button class="tab-b" id="tab-code" onclick="switchTab('code')"></> Code</button> | |
| <div class="tb-url" id="tb-url" style="display:none"> | |
| <span style="font-size:10px">π</span> | |
| <span class="tb-url-t" id="tb-url-t">my-project.buildai.app</span> | |
| </div> | |
| </div> | |
| <div class="tb-right"> | |
| <div class="vp-grp"> | |
| <button class="vp-b on" id="vp-d" onclick="setVP('d')" title="Desktop">π₯</button> | |
| <button class="vp-b" id="vp-t" onclick="setVP('t')" title="Tablet">β¬</button> | |
| <button class="vp-b" id="vp-m" onclick="setVP('m')" title="Mobile">π±</button> | |
| </div> | |
| <button class="tb-tool" onclick="openM('github')">π GitHub</button> | |
| <button class="tb-pub" id="pub-tb" disabled onclick="openM('publish')">π Publish</button> | |
| <div id="tb-user" style="position:relative"> | |
| <button class="tb-signin" onclick="openM('auth','signin')">Sign in</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ MOBILE TABS ββ --> | |
| <div class="mob-tabs" id="mob-tabs"> | |
| <button class="mob-tab on" id="mob-chat" onclick="mobTab('chat')">π¬ Chat</button> | |
| <button class="mob-tab" id="mob-preview" onclick="mobTab('preview')">π Preview</button> | |
| <button class="mob-tab" id="mob-code" onclick="mobTab('code')"></> Code</button> | |
| </div> | |
| <!-- ββ MAIN AREA ββ --> | |
| <div class="main show-chat" id="main-area"> | |
| <!-- SIDEBAR --> | |
| <div class="sb"> | |
| <div class="sb-hd"><span class="sb-lbl">Projects</span></div> | |
| <button class="sb-new" onclick="newProj()">οΌ New Project</button> | |
| <div class="sb-list" id="sb-list"> | |
| <div class="sb-sec">Recent</div> | |
| <div id="sb-empty" class="sb-empty">No projects yet.<br/>Build one to save.</div> | |
| <div id="sb-auth-prompt" class="sb-signin-prompt" style="display:none"> | |
| <div class="sb-sp-t">Sign in to save projects across sessions</div> | |
| <button class="sb-sp-b" onclick="openM('auth','signin')">Sign in free β</button> | |
| </div> | |
| </div> | |
| <div class="sb-ft"> | |
| <div class="sb-user" id="sb-user-box" onclick="openM('auth','signin')"> | |
| <div class="sb-uav" id="sb-av">?</div> | |
| <div style="overflow:hidden;flex:1"> | |
| <div class="sb-un" id="sb-nm">Guest mode</div> | |
| <div class="sb-up" id="sb-plan">Tap to sign in</div> | |
| </div> | |
| </div> | |
| <button class="sb-signout" id="sb-signout" onclick="doSignOut()">Sign out</button> | |
| </div> | |
| </div> | |
| <!-- CHAT PANEL --> | |
| <div class="cp" id="chat-panel"> | |
| <div class="c-stat"> | |
| <div class="r-dots"> | |
| <div class="r-dot" id="rd1">1</div> | |
| <div class="r-dot" id="rd2">2</div> | |
| <div class="r-dot" id="rd3">3</div> | |
| </div> | |
| <div class="c-stat-t" id="c-stat-t">Ready to build</div> | |
| </div> | |
| <div class="c-msgs" id="c-msgs"> | |
| <div class="c-empty" id="c-empty"> | |
| <div class="c-e-ic">ποΈ</div> | |
| <div class="c-e-t">What are we building?</div> | |
| <div class="c-e-s">5 AI models collaborate to create your website with React + Tailwind.</div> | |
| <div class="q-prompts"> | |
| <button class="q-p" onclick="setBP('SaaS landing page with pricing, features, and testimonials')">πΌ SaaS landing page</button> | |
| <button class="q-p" onclick="setBP('Premium e-commerce store with product grid and cart')">π E-commerce store</button> | |
| <button class="q-p" onclick="setBP('Dark analytics dashboard with charts and KPI metrics')">π Analytics dashboard</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="sugg-bar" id="sugg-bar" style="display:none"> | |
| <button class="s-chip" onclick="setBI(this.textContent)">Add Testimonials</button> | |
| <button class="s-chip" onclick="setBI(this.textContent)">Add Pricing Table</button> | |
| <button class="s-chip" onclick="setBI(this.textContent)">Add FAQ Section</button> | |
| <button class="s-chip" onclick="setBI(this.textContent)">Make Dark Theme</button> | |
| <button class="s-chip" onclick="setBI(this.textContent)">Add Animations</button> | |
| <button class="s-chip" onclick="setBI(this.textContent)">Bento Grid Layout</button> | |
| </div> | |
| <div class="c-inp-area"> | |
| <div class="c-inp-row"> | |
| <textarea class="c-ta" id="c-inp" placeholder="Describe your website or request changes..." rows="1" | |
| onkeydown="chatKey(event)" oninput="updSend();autoR(this)"></textarea> | |
| <button class="c-send" id="c-send" onclick="sendMsg()" disabled>β</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- PREVIEW PANEL --> | |
| <div class="pv" id="preview-panel"> | |
| <div class="pub-bar" id="pub-bar" style="display:none"> | |
| <div class="pub-dot"></div> | |
| <span class="pub-t">Published</span> | |
| <span class="pub-dt" id="pub-dt"></span> | |
| </div> | |
| <div class="pv-body" id="pv-body"> | |
| <div class="pv-empty"> | |
| <div class="pv-e-ic">π</div> | |
| <div class="pv-e-t">Preview will appear here</div> | |
| </div> | |
| </div> | |
| <div class="pv-btm" id="pv-btm" style="display:none"> | |
| <button class="dl-btn" onclick="dlFile()">β¬ Download HTML</button> | |
| <button class="dl-zip" onclick="dlZip()">π¦ Download ZIP</button> | |
| <span class="pv-hint">Refine via chat anytime</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ AUTH MODAL ββ --> | |
| <div class="modal-bg" id="modal-auth" style="display:none" onclick="if(event.target===this)closeM('auth')"> | |
| <div class="modal-box" style="max-width:390px"> | |
| <div class="mo-p"> | |
| <div class="mo-hd"> | |
| <div style="display:flex;align-items:center;gap:9px"> | |
| <div style="width:28px;height:28px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px">β‘</div> | |
| <span style="font-weight:800;font-size:17px">BuildAI</span> | |
| </div> | |
| <button class="mo-x" onclick="closeM('auth')">Γ</button> | |
| </div> | |
| <div class="tab-sw"> | |
| <button class="tab-sw-b on" id="at-si" onclick="switchAuth('signin')">Sign in</button> | |
| <button class="tab-sw-b" id="at-su" onclick="switchAuth('signup')">Create account</button> | |
| </div> | |
| <div class="auth-err" id="auth-err"></div> | |
| <div class="auth-ok" id="auth-ok"></div> | |
| <button class="google-btn" onclick="doGoogleAuth()"> | |
| <svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg> | |
| Continue with Google | |
| </button> | |
| <div class="mo-div"><div class="mo-div-l"></div><div class="mo-div-t">or email</div><div class="mo-div-l"></div></div> | |
| <label class="flbl">Email</label> | |
| <input class="finp" id="a-email" type="email" placeholder="you@example.com"/> | |
| <label class="flbl">Password</label> | |
| <input class="finp" id="a-pass" type="password" placeholder="At least 6 characters" onkeydown="if(event.key==='Enter')doAuth()"/> | |
| <div id="name-wrap" style="display:none"> | |
| <label class="flbl">Full name (optional)</label> | |
| <input class="finp" id="a-name" placeholder="Your name"/> | |
| </div> | |
| <button class="mo-btn-p" id="a-sub" onclick="doAuth()">Sign in β</button> | |
| <button class="mo-btn-s" onclick="guestMode();closeM('auth')">Continue as guest (no saving)</button> | |
| <div style="font-size:11px;color:var(--faint);text-align:center;margin-top:14px" id="a-sw-area"> | |
| Don't have an account? <span onclick="switchAuth('signup')" style="color:#818cf8;cursor:pointer;font-weight:600">Sign up free</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ GITHUB MODAL ββ --> | |
| <div class="modal-bg" id="modal-github" style="display:none" onclick="if(event.target===this)closeM('github')"> | |
| <div class="modal-box" style="max-width:440px"> | |
| <div class="mo-p"> | |
| <div class="mo-hd"> | |
| <div style="display:flex;align-items:center;gap:12px"> | |
| <div style="width:40px;height:40px;background:#161b22;border:1px solid #30363d;border-radius:11px;display:flex;align-items:center;justify-content:center;font-size:22px">π</div> | |
| <div><div class="mo-t">Push to GitHub</div><div class="mo-sub">Sync your website to a repo</div></div> | |
| </div> | |
| <button class="mo-x" onclick="closeM('github')">Γ</button> | |
| </div> | |
| <div id="gh-form"> | |
| <label class="flbl">GitHub Personal Access Token</label> | |
| <input class="finp" id="gh-token" placeholder="ghp_xxxxxxxxxxxxxxxxxxxx" style="font-family:'JetBrains Mono',monospace;font-size:12px"/> | |
| <label class="flbl">Repository name</label> | |
| <input class="finp" id="gh-repo" placeholder="my-website"/> | |
| <p style="font-size:11px;color:var(--faint);margin-bottom:16px">Get token at github.com/settings/tokens β needs <code style="background:#090b12;padding:2px 5px;border-radius:4px">repo</code> scope</p> | |
| <button class="mo-btn-p" style="background:#238636" id="gh-btn" onclick="doGithub()">Push to GitHub β</button> | |
| </div> | |
| <div id="gh-done" style="display:none"> | |
| <div class="suc-c"> | |
| <span class="suc-ic">π</span> | |
| <div class="suc-t">Pushed!</div> | |
| <div style="background:#090b12;border:1px solid var(--border2);border-radius:10px;padding:12px;margin-bottom:18px;font-family:'JetBrains Mono',monospace;font-size:12px;color:#818cf8;word-break:break-all" id="gh-url-out"></div> | |
| <button class="mo-btn-p" onclick="closeM('github')">Done</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ PUBLISH MODAL ββ --> | |
| <div class="modal-bg" id="modal-publish" style="display:none" onclick="if(event.target===this)closeM('publish')"> | |
| <div class="modal-box" style="max-width:420px"> | |
| <div class="mo-p"> | |
| <div id="pub-main"> | |
| <div class="mo-hd"> | |
| <div class="mo-t">π Publish Website</div> | |
| <button class="mo-x" onclick="closeM('publish')">Γ</button> | |
| </div> | |
| <p style="font-size:13px;color:var(--muted);margin-bottom:20px">Your site goes live instantly with a unique URL.</p> | |
| <div class="pub-plan on"> | |
| <div class="pub-plan-badge" style="color:#6366f1">Free Β· Instant</div> | |
| <div class="pub-plan-url" id="pub-subdomain-display">my-project.buildai.app</div> | |
| <div class="pub-plan-d">Always-on URL with SSL</div> | |
| </div> | |
| <button class="mo-btn-p" style="background:linear-gradient(135deg,#059669,#10b981)" id="pub-go-btn" onclick="doPublish()">π Publish now</button> | |
| </div> | |
| <div id="pub-done" style="display:none"> | |
| <div style="text-align:right"><button class="mo-x" onclick="closeM('publish')">Γ</button></div> | |
| <div class="suc-c"> | |
| <span class="suc-ic">π</span> | |
| <div class="suc-t">Your site is live!</div> | |
| <div style="padding:13px;background:rgba(99,102,241,.08);border:1px solid rgba(99,102,241,.2);border-radius:12px;margin-bottom:20px;font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;color:#a78bfa;word-break:break-all" id="pub-live-url"></div> | |
| <div style="display:flex;gap:8px"> | |
| <button class="mo-btn-p" style="flex:1" id="pub-open-btn">Open site β</button> | |
| <button class="mo-btn-s" style="flex:1" onclick="closeM('publish')">Done</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ββ ALL JAVASCRIPT ββ --> | |
| <script> | |
| var API = window.location.origin; | |
| var S = { | |
| user: null, | |
| generating: false, | |
| code: '', | |
| tab: 'preview', | |
| vp: 'd', | |
| prompt: '', | |
| projectName: 'New Project', | |
| projectId: null, | |
| versions: [], | |
| versionIdx: -1, | |
| authMode: 'signin', | |
| files: {}, // filename β content | |
| activeFile: null, // currently viewed file | |
| projectType: 'html' // 'html' or 'react' | |
| }; | |
| /* ββ HELPERS ββ */ | |
| function goHome(){ window.location.href = '/'; } | |
| function escH(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } | |
| function slugify(s){ return (s||'project').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,28)||'my-project'; } | |
| function $(id){ return document.getElementById(id); } | |
| /* ββ MOBILE PANEL SWITCHING ββ */ | |
| function mobTab(w){ | |
| ['chat','preview','code'].forEach(function(t){ | |
| var b = $('mob-'+t); | |
| if(b) b.className = 'mob-tab' + (t===w?' on':''); | |
| }); | |
| var m = $('main-area'); | |
| if(!m) return; | |
| if(w === 'chat'){ | |
| m.className = 'main show-chat'; | |
| } else { | |
| m.className = 'main show-preview'; | |
| S.tab = (w === 'code') ? 'code' : 'preview'; | |
| updTabs(); | |
| if(S.code) renderPV(); | |
| } | |
| } | |
| /* ββ TAB SWITCHING ββ */ | |
| function updTabs(){ | |
| ['preview','code'].forEach(function(t){ | |
| var b = $('tab-'+t); | |
| if(b) b.className = 'tab-b' + (t===S.tab?' on':''); | |
| }); | |
| } | |
| function switchTab(t){ | |
| S.tab = t; | |
| updTabs(); | |
| if(t === 'code' && Object.keys(S.files).length > 0){ | |
| renderFileTree(false); | |
| } else if(S.code) { | |
| renderPV(); | |
| } | |
| } | |
| /* ββ VIEWPORT ββ */ | |
| function setVP(v){ | |
| S.vp = v; | |
| ['d','t','m'].forEach(function(x){ | |
| var b = $('vp-'+x); | |
| if(b) b.className = 'vp-b' + (x===v?' on':''); | |
| }); | |
| if(S.code && S.tab==='preview') renderPV(); | |
| } | |
| /* ββ PREVIEW RENDERING ββ */ | |
| function setPV(h){ | |
| var el = $('pv-body'); | |
| if(el) el.innerHTML = h; | |
| } | |
| function renderPV(){ | |
| if(!S.code) return; | |
| var pv = $('pv-body'); | |
| if(!pv) return; | |
| if(S.tab === 'preview'){ | |
| var w = S.vp==='d' ? '100%' : S.vp==='t' ? '768px' : '390px'; | |
| var ws = S.vp!=='d' | |
| ? 'width:'+w+';height:100%;box-shadow:0 0 80px rgba(0,0,0,.8);border-radius:10px;overflow:hidden;flex-shrink:0' | |
| : 'width:100%;height:100%'; | |
| pv.innerHTML = ''; | |
| var wrap = document.createElement('div'); | |
| wrap.setAttribute('style', ws); | |
| var fr = document.createElement('iframe'); | |
| fr.setAttribute('style','width:100%;height:100%;border:none;display:block'); | |
| fr.setAttribute('sandbox','allow-scripts allow-forms allow-popups allow-modals allow-pointer-lock allow-presentation'); | |
| // Use srcdoc β works reliably in all sandboxed iframe environments (HuggingFace, etc.) | |
| // document.write() fails in nested sandboxed iframes; srcdoc does not. | |
| fr.srcdoc = S.code; | |
| wrap.appendChild(fr); | |
| pv.appendChild(wrap); | |
| } else { | |
| // Code view β wrapped in a proper scrollable container | |
| pv.innerHTML = '<div class="code-wrap"><pre class="code-pre">' + escH(S.code) + '</pre></div>'; | |
| } | |
| } | |
| /* ββ STATUS BAR ββ */ | |
| function setStat(msg, round, done){ | |
| var el = $('c-stat-t'); | |
| if(el) el.textContent = msg; | |
| if(round < 1) return; | |
| for(var i=1; i<=3; i++){ | |
| var d = $('rd'+i); | |
| if(!d) continue; | |
| if(i < round || (i===round && done)){ d.className='r-dot dn'; d.textContent='β'; } | |
| else if(i === round){ d.className='r-dot act'; d.textContent=i; } | |
| else{ d.className='r-dot'; d.textContent=i; } | |
| } | |
| } | |
| /* ββ OVERLAY ββ */ | |
| function showOvl(msg, round){ | |
| var pct = Math.min(((round-1)/3)*100, 99) + '%'; | |
| var names = ['Cerebras','Gemini','Groq']; | |
| var badges = ''; | |
| names.forEach(function(n,i){ | |
| var isDone=(round-1)>i, isAct=(round-1)===i; | |
| badges += '<span class="ov-b'+(isAct?' act':'')+'">'+( isDone?'β ':'')+n+'</span>'; | |
| }); | |
| setPV( | |
| '<div class="pv-overlay">' + | |
| '<div class="ov-spin"></div>' + | |
| '<div><div class="ov-t">'+escH(msg)+'</div><div class="ov-s">~30 seconds total</div></div>' + | |
| '<div class="ov-badges">'+badges+'</div>' + | |
| '<div class="ov-bar"><div class="ov-prog" style="width:'+pct+'"></div></div>' + | |
| '</div>' | |
| ); | |
| } | |
| /* ββ VERSION HISTORY ββ */ | |
| function addVer(code, round, label){ | |
| S.versions.push({code:code, round:round, label:label, time:new Date().toLocaleTimeString()}); | |
| S.versionIdx = S.versions.length - 1; | |
| renderVers(); | |
| } | |
| function renderVers(){ | |
| var list = $('ver-list'); | |
| if(!list) return; | |
| if(!S.versions.length){ list.innerHTML='<div class="ver-empty">No versions yet.</div>'; return; } | |
| list.innerHTML = ''; | |
| S.versions.slice().reverse().forEach(function(v,i){ | |
| var ri = S.versions.length-1-i, isCur = ri===S.versionIdx; | |
| var el = document.createElement('div'); | |
| el.className = 'ver-item' + (isCur?' cur':''); | |
| el.innerHTML = | |
| '<div class="ver-dot" style="background:'+(isCur?'#6366f1':'#1e2740')+'"></div>' + | |
| '<div><div class="ver-n">'+escH(v.label)+'</div><div class="ver-d">'+v.time+'</div></div>' + | |
| (isCur ? '<span class="ver-cur-lbl">Current</span>' : ''); | |
| (function(idx){ | |
| el.onclick = function(){ | |
| S.code = S.versions[idx].code; | |
| S.versionIdx = idx; | |
| renderPV(); renderVers(); closeVer(); | |
| }; | |
| })(ri); | |
| list.appendChild(el); | |
| }); | |
| } | |
| function undoVer(){ | |
| if(S.versionIdx > 0){ | |
| S.versionIdx--; | |
| S.code = S.versions[S.versionIdx].code; | |
| renderPV(); renderVers(); | |
| } | |
| } | |
| function toggleVer(e){ | |
| if(e) e.stopPropagation(); | |
| var p = $('ver-panel'); | |
| if(p) p.style.display = (p.style.display==='none'||!p.style.display) ? 'block' : 'none'; | |
| } | |
| function closeVer(){ | |
| var p = $('ver-panel'); | |
| if(p) p.style.display = 'none'; | |
| } | |
| /* ββ SEND / INPUT ββ */ | |
| function updSend(){ | |
| var inp = $('c-inp'), btn = $('c-send'); | |
| if(!inp||!btn) return; | |
| var ok = inp.value.trim() && !S.generating; | |
| btn.className = 'c-send' + (ok?' on':''); | |
| btn.disabled = !ok; | |
| inp.disabled = S.generating; | |
| } | |
| function autoR(el){ el.style.height='auto'; el.style.height=Math.min(el.scrollHeight,80)+'px'; } | |
| function chatKey(e){ if(e.key==='Enter'&&!e.shiftKey){ e.preventDefault(); sendMsg(); } } | |
| function addUMsg(t){ | |
| var m=$('c-msgs'), d=document.createElement('div'); | |
| d.className='m-u'; | |
| d.innerHTML='<div class="m-u-b">'+escH(t)+'</div>'; | |
| m.appendChild(d); m.scrollTop=m.scrollHeight; | |
| } | |
| function setBP(t){ | |
| var el=$('c-inp'); | |
| if(el){ el.value=t; updSend(); autoR(el); setTimeout(sendMsg,80); } | |
| } | |
| function setBI(t){ | |
| var el=$('c-inp'); | |
| if(el){ el.value=t; updSend(); el.focus(); } | |
| } | |
| /* ββ SEND MESSAGE & BUILD ββ */ | |
| function sendMsg(){ | |
| var el=$('c-inp'), t=el.value.trim(); | |
| if(!t||S.generating) return; | |
| el.value=''; updSend(); autoR(el); | |
| addUMsg(t); | |
| S.prompt=t; S.generating=true; S.code=''; S.projectId=null; | |
| var name=t.length>26?t.slice(0,26)+'...':t; | |
| S.projectName=name; | |
| var pn=$('tb-pn'); if(pn) pn.textContent=name; | |
| var ce=$('c-empty'); if(ce) ce.style.display='none'; | |
| ['sugg-bar','pv-btm','pub-bar'].forEach(function(id){var el=$(id);if(el)el.style.display='none';}); | |
| var pb=$('pub-tb'); if(pb) pb.disabled=true; | |
| var tu=$('tb-url'); if(tu) tu.style.display='none'; | |
| setPV( | |
| '<div class="pv-overlay">' + | |
| '<div class="ov-spin"></div>' + | |
| '<div class="ov-t">Connecting to AI models...</div>' + | |
| '<div class="ov-badges">' + | |
| '<span class="ov-b act">Cerebras</span>' + | |
| '<span class="ov-b">Gemini</span>' + | |
| '<span class="ov-b">Groq</span>' + | |
| '</div></div>' | |
| ); | |
| addThinking(); | |
| doFetch(t); | |
| } | |
| /* ββ THINKING INDICATOR ββ */ | |
| var thinkEl = null; | |
| function addThinking(){ | |
| rmThinking(); | |
| var m = $('c-msgs'); | |
| thinkEl = document.createElement('div'); | |
| thinkEl.className = 'm-a'; | |
| thinkEl.id = 'think-el'; | |
| thinkEl.innerHTML = | |
| '<div class="m-a-lbl">BUILDAI</div>' + | |
| '<div class="m-think">' + | |
| '<div class="think-hd"><div class="think-dot"></div>5-model pipeline starting...</div>' + | |
| '<div class="think-ln ap">React + Tailwind Β· Syne + DM Sans Β· Reference engine</div>' + | |
| '<div class="think-ln bl">β‘ Round 1: Cerebras β Ultra-fast builder</div>' + | |
| '<div class="think-ln bl">π§ Round 2: Gemini Flash β Design reviewer</div>' + | |
| '<div class="think-ln bl">β¨ Round 3: Groq β Final polish + SEO</div>' + | |
| '</div>' + | |
| '<div class="typing"><div class="td"></div><div class="td"></div><div class="td"></div></div>'; | |
| m.appendChild(thinkEl); | |
| m.scrollTop = m.scrollHeight; | |
| } | |
| function rmThinking(){ | |
| var t=$('think-el'); | |
| if(t) t.remove(); | |
| thinkEl = null; | |
| } | |
| /* ββ BUILD ERROR ββ */ | |
| function buildErr(msg){ | |
| S.generating=false; rmThinking(); setStat('Build failed',0,false); | |
| var m=$('c-msgs'), el=document.createElement('div'); | |
| el.className='m-a'; | |
| el.innerHTML='<div class="m-a-lbl">BUILDAI</div><div class="m-err">β οΈ '+escH(msg)+'</div>'; | |
| m.appendChild(el); m.scrollTop=m.scrollHeight; | |
| updSend(); | |
| } | |
| /* ββ FETCH / SSE ββ */ | |
| function doFetch(prompt){ | |
| fetch(API+'/api/build', { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({prompt:prompt}) | |
| }) | |
| .then(function(resp){ | |
| if(!resp.ok) throw new Error('Server error '+resp.status+'. Is the Space awake?'); | |
| var reader=resp.body.getReader(), dec=new TextDecoder(), buf=''; | |
| function read(){ | |
| return reader.read().then(function(r){ | |
| if(r.done){ | |
| if(!S.code) buildErr('Build ended without result. Check API keys in HuggingFace Secrets.'); | |
| return; | |
| } | |
| buf += dec.decode(r.value,{stream:true}); | |
| var lines = buf.split('\n'); buf = lines.pop(); | |
| lines.forEach(function(line){ | |
| if(line.startsWith('data: ')){ | |
| try{ handleEv(JSON.parse(line.slice(6))); }catch(e){} | |
| } | |
| }); | |
| return read(); | |
| }); | |
| } | |
| return read(); | |
| }) | |
| .catch(function(e){ buildErr(e.message||'Cannot connect to backend.'); }); | |
| } | |
| function handleEv(d){ | |
| if(d.type==='status'){ setStat(d.message,d.round,false); showOvl(d.message,d.round); } | |
| else if(d.type==='manifest'){ | |
| // Show planned files in file tree early | |
| S.files = {}; S.projectType = d.project_type||'html'; | |
| d.files.forEach(function(f){ S.files[f.name] = ''; }); | |
| renderFileTree(true); | |
| setStat(d.message,1,false); | |
| } | |
| else if(d.type==='file_start'){ | |
| // Mark file as generating | |
| var el = document.querySelector('[data-file="'+d.file+'"]'); | |
| if(el) el.className = 'file-item generating'; | |
| setStat(d.message, 1, false); | |
| } | |
| else if(d.type==='file_done'){ | |
| S.files[d.file] = d.content; | |
| if(!S.activeFile) S.activeFile = d.file; | |
| renderFileTree(false); | |
| var el = document.querySelector('[data-file="'+d.file+'"]'); | |
| if(el){ el.className='file-item done '+fileExtClass(d.file); } | |
| setStat(d.message, 1, false); | |
| } | |
| else if(d.type==='code'){ | |
| S.code=d.code; | |
| if(d.files) S.files = d.files; | |
| if(d.project_type) S.projectType = d.project_type; | |
| addVer(d.code,d.round,d.message); | |
| setStat(d.message,d.round,true); | |
| if(d.final) finish(); | |
| } | |
| else if(d.type==='error'){ buildErr(d.message); } | |
| } | |
| function fileExtClass(fname){ | |
| var ext = (fname.split('.').pop()||'').toLowerCase(); | |
| var map={ts:'file-ext-ts',tsx:'file-ext-tsx',css:'file-ext-css',js:'file-ext-js',html:'file-ext-html',py:'file-ext-py'}; | |
| return map[ext]||''; | |
| } | |
| function renderFileTree(placeholder){ | |
| var pv = $('pv-body'); | |
| if(!pv || S.tab !== 'code') return; | |
| var names = Object.keys(S.files); | |
| if(!names.length) return; | |
| var items = names.map(function(fname){ | |
| var isActive = fname===S.activeFile; | |
| var ext = (fname.split('.').pop()||'').toLowerCase(); | |
| var icon = {ts:'π',tsx:'βοΈ',css:'π¨',js:'π',html:'π',py:'π'}[ext]||'π'; | |
| var status = placeholder ? 'file-item' : (S.files[fname] ? 'file-item done '+fileExtClass(fname) : 'file-item'); | |
| return '<div class="'+status+(isActive?' on':'')+'" data-file="'+escH(fname)+'" onclick="openFile(\''+escH(fname)+'\')" title="'+escH(fname)+'">'+icon+' '+escH(fname.split('/').pop())+'</div>'; | |
| }).join(''); | |
| var tree = '<div class="file-tree"><div class="file-tree-h">π Files</div>'+items+'</div>'; | |
| var content = S.activeFile && S.files[S.activeFile] | |
| ? '<div class="code-wrap" style="flex:1"><pre class="code-pre">'+escH(S.files[S.activeFile])+'</pre></div>' | |
| : '<div class="code-wrap" style="flex:1;display:flex;align-items:center;justify-content:center;color:#3a4060;font-size:12px">Select a file</div>'; | |
| pv.innerHTML = '<div class="code-view">'+tree+content+'</div>'; | |
| } | |
| function openFile(fname){ | |
| S.activeFile = fname; | |
| renderFileTree(false); | |
| } | |
| /* ββ FINISH ββ */ | |
| async function finish(){ | |
| S.generating = false; rmThinking(); | |
| [1,2,3].forEach(function(i){var d=$('rd'+i);if(d){d.className='r-dot dn';d.textContent='β';}}); | |
| setStat('β Website ready!',0,false); | |
| var savedNote = ''; | |
| if(S.user && !S.user.isGuest){ | |
| await autoSave(); | |
| addProjItem(S.projectName, S.projectId, true, S.code); | |
| savedNote = '<div class="m-save-note">β Saved to your account</div>'; | |
| } | |
| var m=$('c-msgs'), ai=document.createElement('div'); | |
| ai.className='m-a'; | |
| ai.innerHTML= | |
| '<div class="m-a-lbl">BUILDAI</div>' + | |
| '<div class="m-card">' + | |
| '<div class="m-card-h">βοΈ '+escH(S.projectName)+'</div>' + | |
| '<div class="m-card-tabs">' + | |
| '<button class="m-card-tb" onclick="switchTab(\'code\');if(window.innerWidth<=768)mobTab(\'code\')">Code</button>' + | |
| '<button class="m-card-tb on" onclick="switchTab(\'preview\');if(window.innerWidth<=768)mobTab(\'preview\')">Preview</button>' + | |
| '</div></div>' + | |
| savedNote + | |
| '<div class="m-txt">Built with React + Tailwind Β· Cerebras + Gemini + Groq pipeline.</div>' + | |
| '<div class="m-next-t">What\'s next?</div>' + | |
| '<div class="m-next-i"><b>Refine:</b> "Make hero darker", "add pricing", "change to blue"</div>' + | |
| '<div class="m-acts">' + | |
| '<button class="m-act primary" onclick="dlZip()">π¦ ZIP</button>' + | |
| '<button class="m-act" onclick="dlFile()">β¬ HTML</button>' + | |
| '<button class="m-act" onclick="openM(\'publish\')">π Publish</button>' + | |
| '<button class="m-act" onclick="openM(\'github\')">π GitHub</button>' + | |
| '</div>'; | |
| m.appendChild(ai); m.scrollTop=m.scrollHeight; | |
| renderPV(); | |
| ['sugg-bar','pv-btm','pub-bar'].forEach(function(id){var el=$(id);if(el)el.style.display='flex';}); | |
| var pb=$('pub-tb'); if(pb) pb.disabled=false; | |
| var tu=$('tb-url'); | |
| if(tu){ | |
| tu.style.display='flex'; | |
| var tt=$('tb-url-t'); if(tt) tt.textContent=slugify(S.projectName)+'.buildai.app'; | |
| } | |
| if(window.innerWidth<=768) setTimeout(function(){mobTab('preview');},500); | |
| updSend(); | |
| } | |
| /* ββ NEW PROJECT ββ */ | |
| function newProj(){ | |
| document.querySelectorAll('.pi').forEach(function(p){p.classList.remove('on');}); | |
| var pn=$('tb-pn'); if(pn) pn.textContent='New Project'; | |
| var cm=$('c-msgs'); | |
| if(cm) cm.innerHTML= | |
| '<div class="c-empty" id="c-empty">' + | |
| '<div class="c-e-ic">ποΈ</div>' + | |
| '<div class="c-e-t">What are we building?</div>' + | |
| '<div class="c-e-s">Describe your website below.</div>' + | |
| '<div class="q-prompts">' + | |
| '<button class="q-p" onclick="setBP(\'SaaS landing page with pricing\')">πΌ SaaS</button>' + | |
| '<button class="q-p" onclick="setBP(\'E-commerce store with products\')">π E-commerce</button>' + | |
| '<button class="q-p" onclick="setBP(\'Analytics dashboard\')">π Dashboard</button>' + | |
| '</div></div>'; | |
| S.code=''; S.generating=false; S.versions=[]; S.versionIdx=-1; S.projectId=null; S.projectName='New Project'; | |
| [1,2,3].forEach(function(i){var d=$('rd'+i);if(d){d.className='r-dot';d.textContent=i;}}); | |
| setStat('Ready to build',0,false); | |
| ['sugg-bar','pv-btm','pub-bar'].forEach(function(id){var el=$(id);if(el)el.style.display='none';}); | |
| var pb=$('pub-tb'); if(pb) pb.disabled=true; | |
| var tu=$('tb-url'); if(tu) tu.style.display='none'; | |
| renderVers(); | |
| setPV('<div class="pv-empty"><div class="pv-e-ic">π</div><div class="pv-e-t">Preview will appear here</div></div>'); | |
| if(window.innerWidth<=768) mobTab('chat'); | |
| updSend(); | |
| } | |
| /* ββ SIDEBAR PROJECTS ββ */ | |
| function clearSBProjects(){ | |
| $('sb-list').querySelectorAll('.pi').forEach(function(p){p.remove();}); | |
| var se=$('sb-empty'); if(se) se.style.display='block'; | |
| } | |
| function addProjItem(name, id, makeCurrent, code){ | |
| var se=$('sb-empty'); if(se) se.style.display='none'; | |
| if(makeCurrent) document.querySelectorAll('.pi').forEach(function(p){p.classList.remove('on');}); | |
| var colors=['#6366f1','#8b5cf6','#10b981','#f59e0b','#ec4899']; | |
| var c = colors[Math.floor(Math.random()*colors.length)]; | |
| var el = document.createElement('div'); | |
| el.className='pi'+(makeCurrent?' on':''); | |
| el.dataset.id=id||''; el.dataset.code=code||''; | |
| el.innerHTML= | |
| '<div class="pi-dot" style="background:'+c+'"></div>' + | |
| '<div style="overflow:hidden;flex:1">' + | |
| '<div class="pi-n">'+escH(name)+'</div>' + | |
| '<div class="pi-d">'+new Date().toLocaleDateString('en-US',{day:'numeric',month:'short'})+'</div>' + | |
| '</div>'; | |
| el.onclick = function(){ | |
| document.querySelectorAll('.pi').forEach(function(p){p.classList.remove('on');}); | |
| el.classList.add('on'); | |
| var pn=$('tb-pn'); if(pn) pn.textContent=name; | |
| if(el.dataset.code){ | |
| S.code=el.dataset.code; S.projectName=name; S.projectId=el.dataset.id; | |
| renderPV(); | |
| var pb=$('pv-btm'); if(pb) pb.style.display='flex'; | |
| var ptb=$('pub-tb'); if(ptb) ptb.disabled=false; | |
| if(window.innerWidth<=768) mobTab('preview'); | |
| } | |
| }; | |
| var list=$('sb-list'), first=list.querySelector('.pi'); | |
| if(first) list.insertBefore(el,first); else list.appendChild(el); | |
| } | |
| /* ββ DOWNLOAD ββ */ | |
| function dlFile(){ | |
| if(!S.code) return; | |
| var blob=new Blob([S.code],{type:'text/html'}), url=URL.createObjectURL(blob); | |
| var a=document.createElement('a'); a.href=url; a.download=slugify(S.projectName)+'.html'; a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| function dlZip(){ | |
| if(!S.code && !Object.keys(S.files).length){ return; } | |
| if(typeof JSZip==='undefined'){ alert('JSZip not loaded. Downloading HTML instead.'); dlFile(); return; } | |
| try{ | |
| var zip=new JSZip(), slug=slugify(S.projectName); | |
| if(Object.keys(S.files).length > 1){ | |
| Object.keys(S.files).forEach(function(fname){ | |
| if(S.files[fname]) zip.file(fname, S.files[fname]); | |
| }); | |
| zip.file('README.md','# '+slug+'\nBuilt with BuildAI.\n\nOpen index.html in a browser or deploy to Netlify/Vercel/GitHub Pages.'); | |
| } else { | |
| var code=S.code; | |
| var styleMatches=code.match(/<style[^>]*>([\s\S]*?)<\/style>/gi)||[]; | |
| var css='/* BuildAI Generated */\n\n', htmlNo=code; | |
| styleMatches.forEach(function(m,i){ | |
| css+=m.replace(/<style[^>]*>/i,'').replace(/<\/style>/i,'')+'\n'; | |
| if(i===0) htmlNo=htmlNo.replace(m,'<link rel="stylesheet" href="style.css"/>'); | |
| else htmlNo=htmlNo.replace(m,''); | |
| }); | |
| zip.file('index.html',htmlNo); zip.file('style.css',css); | |
| zip.file('README.md','# '+slug+'\nBuilt with BuildAI.\n\nOpen index.html in browser.'); | |
| } | |
| zip.generateAsync({type:'blob'}).then(function(blob){ | |
| var url=URL.createObjectURL(blob), a=document.createElement('a'); | |
| a.href=url; a.download=slug+'.zip'; a.click(); URL.revokeObjectURL(url); | |
| }); | |
| }catch(e){ console.error(e); dlFile(); } | |
| } | |
| /* ββ MODALS ββ */ | |
| function openM(name, extra){ | |
| if(name==='auth'){ S.authMode=extra||'signin'; switchAuth(S.authMode); } | |
| if(name==='publish'){ | |
| var pd=$('pub-subdomain-display'); | |
| if(pd) pd.textContent=slugify(S.projectName)+'.buildai.app'; | |
| } | |
| var el=$('modal-'+name); | |
| if(el) el.style.display='flex'; | |
| } | |
| function closeM(name){ | |
| var el=$('modal-'+name); | |
| if(el) el.style.display='none'; | |
| if(name==='publish'){ | |
| var pm=$('pub-main'),pd=$('pub-done'),pb=$('pub-go-btn'); | |
| if(pm) pm.style.display='block'; if(pd) pd.style.display='none'; | |
| if(pb){ pb.textContent='π Publish now'; pb.disabled=false; } | |
| } | |
| if(name==='github'){ | |
| var gf=$('gh-form'),gd=$('gh-done'),gb=$('gh-btn'); | |
| if(gf) gf.style.display='block'; if(gd) gd.style.display='none'; | |
| if(gb){ gb.textContent='Push to GitHub β'; gb.disabled=false; } | |
| } | |
| } | |
| function switchAuth(mode){ | |
| S.authMode=mode; | |
| var si=$('at-si'), su=$('at-su'), nw=$('name-wrap'), sub=$('a-sub'), sw=$('a-sw-area'); | |
| if(si) si.className='tab-sw-b'+(mode==='signin'?' on':''); | |
| if(su) su.className='tab-sw-b'+(mode==='signup'?' on':''); | |
| if(nw) nw.style.display=mode==='signup'?'block':'none'; | |
| if(sub) sub.textContent=mode==='signin'?'Sign in β':'Create account β'; | |
| if(sw) sw.innerHTML=mode==='signin' | |
| ?"Don't have an account? <span onclick=\"switchAuth('signup')\" style='color:#818cf8;cursor:pointer;font-weight:600'>Sign up free</span>" | |
| :"Already have an account? <span onclick=\"switchAuth('signin')\" style='color:#818cf8;cursor:pointer;font-weight:600'>Sign in</span>"; | |
| var ae=$('auth-err'), ao=$('auth-ok'); | |
| if(ae) ae.style.display='none'; if(ao) ao.style.display='none'; | |
| } | |
| function showAuthErr(msg){ | |
| var el=$('auth-err'); | |
| if(el){ el.textContent='β οΈ '+msg; el.style.display='block'; } | |
| } | |
| /* ββ PUBLISH ββ */ | |
| async function doPublish(){ | |
| if(!S.code){ alert('Build a website first.'); return; } | |
| var slug=slugify(S.projectName), btn=$('pub-go-btn'); | |
| if(btn){ btn.textContent='Publishing...'; btn.disabled=true; } | |
| try{ | |
| var r=await fetch(API+'/api/publish',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({project_id:slug,html_code:S.code,subdomain:slug})}); | |
| var d=await r.json(); | |
| var pm=$('pub-main'), pd=$('pub-done'); | |
| if(pm) pm.style.display='none'; if(pd) pd.style.display='block'; | |
| var url=d.url||'https://'+slug+'.buildai.app'; | |
| var lu=$('pub-live-url'), ob=$('pub-open-btn'); | |
| if(lu) lu.textContent=url; | |
| if(ob) ob.onclick=function(){window.open(url,'_blank');}; | |
| var pb=$('pub-bar'), pdt=$('pub-dt'); | |
| if(pb) pb.style.display='flex'; if(pdt) pdt.textContent=new Date().toLocaleTimeString(); | |
| }catch(e){ | |
| if(btn){ btn.textContent='π Publish now'; btn.disabled=false; } | |
| alert('Publish failed: '+e.message); | |
| } | |
| } | |
| /* ββ GITHUB ββ */ | |
| async function doGithub(){ | |
| var token=$('gh-token').value.trim(), repo=$('gh-repo').value.trim(); | |
| if(!token||!repo){ alert('Enter GitHub token and repo name.'); return; } | |
| if(!S.code){ alert('Build a website first.'); return; } | |
| var btn=$('gh-btn'); | |
| if(btn){ btn.textContent='Pushing...'; btn.disabled=true; } | |
| try{ | |
| var r=await fetch(API+'/api/github/sync',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({project_id:'demo',html_code:S.code,repo_name:repo,github_token:token})}); | |
| var d=await r.json(); | |
| if(d.success){ | |
| var gf=$('gh-form'),gd=$('gh-done'),gu=$('gh-url-out'); | |
| if(gf) gf.style.display='none'; if(gd) gd.style.display='block'; if(gu) gu.textContent=d.repo_url; | |
| }else{ | |
| alert('Failed: '+(d.detail||'Unknown')); | |
| if(btn){ btn.textContent='Push to GitHub β'; btn.disabled=false; } | |
| } | |
| }catch(e){ | |
| alert('Error: '+e.message); | |
| if(btn){ btn.textContent='Push to GitHub β'; btn.disabled=false; } | |
| } | |
| } | |
| /* ββ AUTH ββ */ | |
| function guestMode(){ | |
| S.user={id:'guest-'+Date.now(),email:'guest',name:'Guest',isGuest:true}; | |
| var sn=$('sb-nm'),sp=$('sb-plan'),sa=$('sb-av'),su=$('sb-user-box'),tu=$('tb-user'); | |
| if(sn) sn.textContent='Guest mode'; | |
| if(sp) sp.textContent='Tap to sign in'; | |
| if(sa) sa.textContent='?'; | |
| if(su) su.onclick=function(){openM('auth','signin');}; | |
| if(tu) tu.innerHTML='<button class="tb-guest" onclick="openM(\'auth\',\'signin\')">π€ Sign in</button>'; | |
| var sap=$('sb-auth-prompt'); if(sap) sap.style.display='block'; | |
| } | |
| function doSignOut(){ | |
| if(typeof fbAuth!=='undefined'){ fbAuth.signOut().catch(function(e){console.error(e);}); } | |
| else{ window.location.reload(); } | |
| } | |
| function toggleUD(){ | |
| var d=$('user-dd'); | |
| if(d) d.style.display=d.style.display==='none'?'block':'none'; | |
| } | |
| function doGoogleAuth(){ | |
| if(typeof fbAuth==='undefined'){ alert('Auth not loaded. Please wait a moment.'); return; } | |
| var provider=new firebase.auth.GoogleAuthProvider(); | |
| provider.setCustomParameters({prompt:'select_account'}); | |
| var isMobile=/Android|iPhone|iPad|iPod/i.test(navigator.userAgent); | |
| if(isMobile){ | |
| fbAuth.signInWithRedirect(provider).catch(function(e){showAuthErr(e.message);}); | |
| }else{ | |
| fbAuth.signInWithPopup(provider).then(function(){closeM('auth');}).catch(function(e){ | |
| if(e.code==='auth/popup-closed-by-user') return; | |
| if(e.code==='auth/unauthorized-domain'){ | |
| showAuthErr('Add your HuggingFace domain to Firebase Console β Authentication β Authorized domains.'); | |
| }else{ | |
| showAuthErr(e.message||'Google sign in failed.'); | |
| } | |
| }); | |
| } | |
| } | |
| async function doAuth(){ | |
| if(typeof fbAuth==='undefined'){ alert('Auth not loaded. Please wait.'); return; } | |
| var email=$('a-email').value.trim(), pass=$('a-pass').value; | |
| var ae=$('auth-err'), ao=$('auth-ok'); | |
| if(ae) ae.style.display='none'; if(ao) ao.style.display='none'; | |
| if(!email||!pass){ showAuthErr('Enter email and password.'); return; } | |
| if(pass.length<6){ showAuthErr('Password must be at least 6 characters.'); return; } | |
| var btn=$('a-sub'); | |
| if(btn){ btn.disabled=true; btn.textContent='Please wait...'; } | |
| try{ | |
| if(S.authMode==='signup'){ await fbAuth.createUserWithEmailAndPassword(email,pass); } | |
| else{ await fbAuth.signInWithEmailAndPassword(email,pass); } | |
| closeM('auth'); | |
| }catch(e){ | |
| var msgs={ | |
| 'auth/email-already-in-use':'Account already exists. Sign in instead.', | |
| 'auth/user-not-found':'No account found with that email.', | |
| 'auth/invalid-credential':'Wrong email or password.', | |
| 'auth/wrong-password':'Wrong password. Try again.', | |
| 'auth/invalid-email':'Invalid email address.', | |
| 'auth/weak-password':'Password too weak. Use 6+ characters.', | |
| 'auth/too-many-requests':'Too many attempts. Try again later.' | |
| }; | |
| showAuthErr(msgs[e.code]||e.message||'Something went wrong.'); | |
| } | |
| if(btn){ btn.disabled=false; btn.textContent=S.authMode==='signin'?'Sign in β':'Create account β'; } | |
| } | |
| /* ββ FIRESTORE PROJECT SAVING ββ */ | |
| async function loadProjects(){ | |
| if(!S.user||S.user.isGuest||typeof fbDb==='undefined') return; | |
| try{ | |
| var snap=await fbDb.collection('projects').where('user_id','==',S.user.id).orderBy('updated_at','desc').limit(20).get(); | |
| clearSBProjects(); | |
| if(!snap.empty){ | |
| var se=$('sb-empty'); if(se) se.style.display='none'; | |
| snap.forEach(function(doc){ | |
| var d=doc.data(); | |
| addProjItem(d.name,doc.id,false,d.html_code); | |
| }); | |
| } | |
| }catch(e){ console.warn('Load projects:',e.message); } | |
| } | |
| async function autoSave(){ | |
| if(!S.user||S.user.isGuest||!S.code||typeof fbDb==='undefined') return; | |
| try{ | |
| var data={user_id:S.user.id,name:S.projectName,html_code:S.code,updated_at:firebase.firestore.FieldValue.serverTimestamp()}; | |
| if(S.projectId){ | |
| await fbDb.collection('projects').doc(S.projectId).update(data); | |
| }else{ | |
| data.created_at=firebase.firestore.FieldValue.serverTimestamp(); | |
| var ref=await fbDb.collection('projects').add(data); | |
| S.projectId=ref.id; | |
| } | |
| }catch(e){ console.warn('Save:',e.message); } | |
| } | |
| /* ββ AUTH STATE CALLBACKS ββ */ | |
| function onSignedIn(user){ | |
| S.user={id:user.uid,email:user.email,name:user.displayName||user.email.split('@')[0],isGuest:false}; | |
| var init=S.user.name[0].toUpperCase(); | |
| var tu=$('tb-user'); | |
| if(tu) tu.innerHTML= | |
| '<div class="tb-av" onclick="toggleUD()" title="'+escH(S.user.email)+'">' + | |
| init + | |
| '<div class="user-dd" id="user-dd" style="display:none">' + | |
| '<div class="ud-head">' + | |
| '<div class="ud-name">'+escH(S.user.name)+'</div>' + | |
| '<div class="ud-email">'+escH(S.user.email)+'</div>' + | |
| '</div>' + | |
| '<div class="ud-item" onclick="openM(\'github\')">π GitHub sync</div>' + | |
| '<div class="ud-sep"></div>' + | |
| '<div class="ud-item red" onclick="doSignOut()">Sign out</div>' + | |
| '</div></div>'; | |
| var sa=$('sb-av'),sn=$('sb-nm'),sp=$('sb-plan'),so=$('sb-signout'),su=$('sb-user-box'),sap=$('sb-auth-prompt'); | |
| if(sa) sa.textContent=init; | |
| if(sn) sn.textContent=S.user.name; | |
| if(sp) sp.textContent='Free plan'; | |
| if(so) so.style.display='block'; | |
| if(su) su.onclick=null; | |
| if(sap) sap.style.display='none'; | |
| loadProjects(); | |
| } | |
| function onSignedOut(){ | |
| S.user=null; | |
| var tu=$('tb-user'); | |
| if(tu) tu.innerHTML='<button class="tb-signin" onclick="openM(\'auth\',\'signin\')">Sign in</button>'; | |
| var sa=$('sb-av'),sn=$('sb-nm'),sp=$('sb-plan'),so=$('sb-signout'),su=$('sb-user-box'),sap=$('sb-auth-prompt'); | |
| if(sa) sa.textContent='?'; | |
| if(sn) sn.textContent='Guest mode'; | |
| if(sp) sp.textContent='Tap to sign in'; | |
| if(so) so.style.display='none'; | |
| if(su) su.onclick=function(){openM('auth','signin');}; | |
| if(sap) sap.style.display='block'; | |
| clearSBProjects(); | |
| } | |
| /* ββ CLOSE DROPDOWNS ON OUTSIDE CLICK ββ */ | |
| document.addEventListener('click',function(e){ | |
| var vp=$('ver-panel'), vb=$('ver-btn'); | |
| if(vp&&vp.style.display!=='none'&&!vp.contains(e.target)&&vb&&!vb.contains(e.target)) vp.style.display='none'; | |
| var dd=$('user-dd'); | |
| if(dd&&dd.style.display!=='none'&&!dd.parentElement.contains(e.target)) dd.style.display='none'; | |
| }); | |
| /* ββ INIT ββ */ | |
| (function initUI(){ | |
| S.tab='preview'; S.code=''; updTabs(); | |
| var pvBody=$('pv-body'); | |
| if(pvBody) pvBody.innerHTML='<div class="pv-empty"><div class="pv-e-ic">π</div><div class="pv-e-t">Preview will appear here</div></div>'; | |
| ['pv-btm','pub-bar','sugg-bar'].forEach(function(id){var el=$(id);if(el)el.style.display='none';}); | |
| var pb=$('pub-tb'); if(pb) pb.disabled=true; | |
| var tu=$('tb-url'); if(tu) tu.style.display='none'; | |
| [1,2,3].forEach(function(i){var d=$('rd'+i);if(d){d.className='r-dot';d.textContent=i;}}); | |
| setStat('Ready to build',0,false); | |
| updSend(); | |
| })(); | |
| /* ββ PROMPT FROM LANDING PAGE ββ */ | |
| (function(){ | |
| try{ | |
| var saved=sessionStorage.getItem('bai_prompt'); | |
| if(saved){ sessionStorage.removeItem('bai_prompt'); setTimeout(function(){setBP(saved);},400); } | |
| else{ | |
| var params=new URLSearchParams(window.location.search); | |
| var qp=params.get('prompt'); | |
| if(qp) setTimeout(function(){setBP(qp);},400); | |
| } | |
| }catch(e){} | |
| })(); | |
| </script> | |
| <!-- ββ FIREBASE ββ --> | |
| <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-auth-compat.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
| <script> | |
| try{ | |
| firebase.initializeApp({ | |
| apiKey:"AIzaSyDGtOYRxO2N6qPDNwkuZmwkhQelmJ9VQkQ", | |
| authDomain:"buildai-b1d98.firebaseapp.com", | |
| projectId:"buildai-b1d98", | |
| storageBucket:"buildai-b1d98.firebasestorage.app", | |
| messagingSenderId:"511431818463", | |
| appId:"1:511431818463:web:846a142888fc3517353d6a" | |
| }); | |
| var fbAuth=firebase.auth(); | |
| var fbDb=firebase.firestore(); | |
| fbAuth.onAuthStateChanged(function(user){if(user) onSignedIn(user); else onSignedOut();}); | |
| fbAuth.getRedirectResult().then(function(r){if(r&&r.user) closeM('auth');}).catch(function(e){ | |
| if(e.code&&e.code!=='auth/no-auth-event') console.warn('Redirect:',e.code); | |
| }); | |
| }catch(e){ | |
| console.warn('Firebase failed:',e.message); | |
| var tu=document.getElementById('tb-user'); | |
| if(tu) tu.innerHTML='<button class="tb-guest" onclick="guestMode()">Use as Guest</button>'; | |
| } | |
| </script> | |
| </body> | |
| </html> | |