Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"/> | |
| <title>AI UX Prototyper</title> | |
| <link rel="stylesheet" href="/static/suite.css"> | |
| <style> | |
| :root{--bg:#f7f7f8;--surface:#ffffff;--text:#0f172a;--muted:#6b7280;--border:#e5e7eb;--brand:#111827;--radius:12px;--space-1:8px;--space-2:12px;--space-3:16px;--space-4:24px;--space-5:32px;--shadow-1:0 1px 2px rgba(0,0,0,.05),0 1px 3px rgba(0,0,0,.08)} | |
| *{box-sizing:border-box} | |
| body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);margin:0} | |
| .container{max-width:1400px;margin:0 auto;padding:var(--space-5) var(--space-5)} | |
| .header{margin-bottom:var(--space-4)} | |
| .header h1{margin:0} | |
| .header p{margin:.5rem 0 0;color:var(--muted)} | |
| .card,.pane{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-1)} | |
| .card{padding:var(--space-4)} | |
| label{display:block;margin:var(--space-2) 0 var(--space-1);font-weight:600} | |
| input,textarea{width:100%;padding:var(--space-2);border:1px solid var(--border);border-radius:8px;font-size:16px;background:#fff} | |
| select{width:100%;padding:var(--space-2);border:1px solid var(--border);border-radius:8px;font-size:16px;background:#fff} | |
| input[type=color]{width:56px;height:40px;padding:0;border:1px solid var(--border);border-radius:8px;background:#fff} | |
| .actions{display:flex;gap:var(--space-2);flex-wrap:wrap;margin-top:var(--space-2)} | |
| .btn{appearance:none;border:0;border-radius:8px;padding:10px 16px;font-weight:600;cursor:pointer} | |
| .btn--primary{background:var(--brand);color:#fff} | |
| .btn--ghost{background:transparent;color:var(--brand);border:1px solid var(--border)} | |
| .grid{display:grid;grid-template-columns:1fr;gap:var(--space-3);margin-top:var(--space-3)} | |
| @media (min-width:900px){.grid{grid-template-columns:1fr 1fr}} | |
| @media (min-width:1200px){.grid{grid-template-columns:1.25fr 1fr}} | |
| .pane{padding:var(--space-3);min-height:240px;overflow:auto} | |
| .pane h3{margin-top:0} | |
| .toolbar{display:flex;gap:var(--space-2);align-items:center;margin-bottom:var(--space-2)} | |
| pre{white-space:pre-wrap;background:#fafafa;border:1px dashed var(--border);padding:var(--space-2);border-radius:8px} | |
| details{margin-top:var(--space-3)} | |
| img{max-width:100%;border:1px solid var(--border);border-radius:8px} | |
| /* Shared suite header */ | |
| .site-header{position:sticky;top:0;background:var(--surface);border-bottom:1px solid var(--border);z-index:50} | |
| .site-header .nav{display:flex;align-items:center;justify-content:space-between;max-width:1400px;margin:0 auto;padding:12px 24px} | |
| .site-header .brand a{text-decoration:none;color:var(--text)} | |
| .site-header .links{display:flex;gap:16px} | |
| .site-header .links a{text-decoration:none;color:var(--muted);padding:6px 10px;border-radius:8px} | |
| .site-header .links a:hover{background:#f3f4f6;color:var(--text)} | |
| </style> | |
| <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> | |
| </head> | |
| <body> | |
| <div id="suite-shared-header"></div> | |
| <script src="/static/header.js"></script> | |
| <div class="container"> | |
| <header class="header"> | |
| <h1>AI UX Prototyper</h1> | |
| <p>Describe a feature to generate a clickable wireframe and a flow diagram.</p> | |
| </header> | |
| <section class="card"> | |
| <form id="form"> | |
| <label>Feature</label> | |
| <input id="feature" placeholder="e.g., voice assistant for claims" required /> | |
| <label>Constraints (optional)</label> | |
| <textarea id="constraints" rows="2" placeholder="e.g., mobile first; privacy by design"></textarea> | |
| <div class="grid" style="grid-template-columns:1fr 1fr;gap:12px;margin-top:8px"> | |
| <div> | |
| <label>Company (text only)</label> | |
| <input id="company" placeholder="e.g., Flagship" /> | |
| </div> | |
| <div> | |
| <label>Accent color</label> | |
| <input id="accent" type="color" value="#3b82f6" /> | |
| </div> | |
| <div> | |
| <label>Palette</label> | |
| <select id="palette"> | |
| <option value="neutral" selected>Neutral</option> | |
| <option value="cool">Cool</option> | |
| <option value="warm">Warm</option> | |
| <option value="dark">Dark</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Tone</label> | |
| <select id="tone"> | |
| <option value="professional" selected>Professional</option> | |
| <option value="friendly">Friendly</option> | |
| <option value="formal">Formal</option> | |
| <option value="playful">Playful</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="actions"> | |
| <button type="submit" class="btn btn--primary">Generate Prototype</button> | |
| </div> | |
| </form> | |
| </section> | |
| <section class="grid"> | |
| <div class="pane"> | |
| <h3>Wireframe</h3> | |
| <div class="toolbar"> | |
| <button id="openNew" class="btn btn--ghost">Open in new tab</button> | |
| </div> | |
| <div id="wire"></div> | |
| <div id="imgWrap" style="display:none; margin-top:8px"><img id="wireImg" alt="Wireframe image"/></div> | |
| </div> | |
| <div class="pane"> | |
| <h3>Flow</h3> | |
| <div id="flowImgWrap" style="display:none; margin-top:8px"><img id="flowImg" alt="Flow image"/></div> | |
| <div id="mermaidSvg" style="display:none"></div> | |
| <pre id="mermaid"></pre> | |
| </div> | |
| </section> | |
| <details><summary>Raw JSON</summary><pre id="raw"></pre></details> | |
| </div> | |
| <script> | |
| const form = document.getElementById('form'); | |
| const feature = document.getElementById('feature'); | |
| const constraints = document.getElementById('constraints'); | |
| const wire = document.getElementById('wire'); | |
| let lastWireHtml = ''; | |
| const mm = document.getElementById('mermaid'); | |
| const mmSvg = document.getElementById('mermaidSvg'); | |
| const raw = document.getElementById('raw'); | |
| const openNew = document.getElementById('openNew'); | |
| const imgWrap = document.getElementById('imgWrap'); | |
| const wireImg = document.getElementById('wireImg'); | |
| const flowImgWrap = document.getElementById('flowImgWrap'); | |
| const flowImg = document.getElementById('flowImg'); | |
| const company = document.getElementById('company'); | |
| const accent = document.getElementById('accent'); | |
| const paletteSel = document.getElementById('palette'); | |
| const toneSel = document.getElementById('tone'); | |
| form.addEventListener('submit', async (e)=>{ | |
| e.preventDefault(); | |
| // Set loading state | |
| const submitBtn = form.querySelector('button[type="submit"]'); | |
| const originalText = submitBtn.textContent; | |
| submitBtn.textContent = 'Generating...'; | |
| submitBtn.disabled = true; | |
| wire.innerHTML = '(loading)'; | |
| mm.textContent = '(loading)'; | |
| mmSvg.style.display='none'; | |
| mmSvg.innerHTML=''; | |
| flowImgWrap.style.display='none'; | |
| raw.textContent='(loading)'; | |
| try{ | |
| const savedKey = (function(){ try { return (localStorage.getItem('OPENAI_API_KEY')||'').trim(); } catch(e){ return ''; } })(); | |
| const r = await fetch('/ux/prototype', {method:'POST', headers:{'Content-Type':'application/json', 'Authorization': savedKey ? ('Bearer ' + savedKey) : undefined, 'x-openai-key': savedKey || ''}, body: JSON.stringify({ feature: feature.value.trim(), constraints: constraints.value.trim(), company: (company.value||'').trim() || undefined, accent_color: accent.value, palette: paletteSel.value, tone: toneSel.value, api_key: savedKey || null })}); | |
| const text = await r.text(); | |
| let data = {}; | |
| try{ data = text ? JSON.parse(text) : {}; }catch{ data = { error: text } } | |
| raw.textContent = JSON.stringify(data, null, 2); | |
| if(!r.ok){ wire.textContent='Error'; mm.textContent='Error'; flowImgWrap.style.display='none'; mm.style.display='block'; return; } | |
| // Isolate prototype CSS/JS in an iframe so it cannot affect the host page | |
| lastWireHtml = data.wireframe_html || ''; | |
| console.log('Wireframe HTML length:', lastWireHtml.length); | |
| console.log('Wireframe HTML preview:', lastWireHtml.substring(0, 200)); | |
| wire.innerHTML = ''; | |
| if (lastWireHtml) { | |
| const frame = document.createElement('iframe'); | |
| frame.setAttribute('title','Wireframe Preview'); | |
| frame.setAttribute('referrerpolicy','no-referrer'); | |
| // Keep strict sandbox (no same-origin) so prototype CSS/JS cannot touch host page | |
| frame.setAttribute('sandbox','allow-scripts allow-forms allow-modals allow-popups'); | |
| frame.style.width = '100%'; | |
| frame.style.height = '800px'; | |
| frame.style.border = '1px solid var(--border)'; | |
| frame.srcdoc = lastWireHtml; | |
| wire.appendChild(frame); | |
| } | |
| mm.textContent = data.flow_mermaid; | |
| if (data.wireframe_image_data_url) { | |
| wireImg.onerror = ()=>{ imgWrap.style.display='none'; }; | |
| wireImg.src = data.wireframe_image_data_url; | |
| imgWrap.style.display='block'; | |
| } else { imgWrap.style.display='none'; } | |
| if (data.flow_image_data_url) { | |
| flowImg.onerror = ()=>{ flowImgWrap.style.display='none'; mm.style.display='block'; }; | |
| flowImg.src = data.flow_image_data_url; | |
| flowImgWrap.style.display='block'; | |
| mm.style.display='none'; | |
| } else { | |
| flowImgWrap.style.display='none'; | |
| // Render Mermaid to SVG if possible | |
| try { | |
| if (window.mermaid && data.flow_mermaid && data.flow_mermaid.trim().startsWith('graph')){ | |
| window.mermaid.initialize({ startOnLoad:false, theme:'base' }); | |
| const id = 'mmd-' + Math.random().toString(36).slice(2); | |
| window.mermaid.render(id, data.flow_mermaid) | |
| .then(({ svg })=>{ | |
| mmSvg.innerHTML = svg; | |
| mmSvg.style.display='block'; | |
| mm.style.display='none'; | |
| }) | |
| .catch(()=>{ | |
| mmSvg.style.display='none'; | |
| mm.style.display='block'; | |
| }); | |
| } else { | |
| mmSvg.style.display='none'; | |
| mm.style.display='block'; | |
| } | |
| } catch (e){ | |
| mmSvg.style.display='none'; | |
| mm.style.display='block'; | |
| } | |
| } | |
| }catch(err){ wire.textContent=String(err); mm.textContent=''; raw.textContent=''; } | |
| finally { | |
| // Reset button state | |
| submitBtn.textContent = originalText; | |
| submitBtn.disabled = false; | |
| } | |
| }); | |
| openNew.addEventListener('click', ()=>{ | |
| if (!lastWireHtml) { | |
| alert('No wireframe content available. Please generate a prototype first.'); | |
| return; | |
| } | |
| // Use the wireframe HTML directly - it already contains complete HTML structure | |
| const fullHtml = lastWireHtml; | |
| try { | |
| // Create a blob URL | |
| const blob = new Blob([fullHtml], { type: 'text/html' }); | |
| const url = URL.createObjectURL(blob); | |
| // Create a temporary link and click it | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.target = '_blank'; | |
| // Remove download attribute to open in new tab instead of downloading | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| // Clean up the blob URL after a short delay | |
| setTimeout(() => URL.revokeObjectURL(url), 1000); | |
| } catch (err) { | |
| console.error('Error creating new tab:', err); | |
| // Fallback: try window.open with user gesture | |
| try { | |
| const w = window.open('', '_blank'); | |
| if (w) { | |
| w.document.open(); | |
| w.document.write(fullHtml); | |
| w.document.close(); | |
| } else { | |
| alert('Unable to open new tab. Please check your browser settings or try copying the content manually.'); | |
| } | |
| } catch (fallbackErr) { | |
| alert('Error opening new tab: ' + fallbackErr.message); | |
| } | |
| } | |
| }); | |
| // Removed non-working image toggle | |
| </script> | |
| </body> | |
| </html> | |