| |
| """ |
| RealWebLearner: learned char n-gram intent model + compositional HTML/CSS/JS generator. |
| |
| It learns web-development intent from many generated examples and then composes code. |
| It is designed for this tiny CPU workspace and integrates with unified_learning_ai.py. |
| |
| Usage: |
| python real_web_learner.py --mode train --out outputs/real_web_learner |
| python real_web_learner.py --mode ask --out outputs/real_web_learner --prompt "create a responsive landing page with dark mode" |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import re |
| from pathlib import Path |
| from typing import List, Tuple |
|
|
| from real_python_learner import NBIntent |
|
|
| WEB_LABELS = [ |
| "full_page", "landing_page", "portfolio", "navbar", "hero", "responsive_grid", |
| "card", "form_validation", "todo_app", "dark_mode", "modal", "tabs", |
| "accordion", "carousel", "css_animation", "dashboard", "fetch_api", |
| "counter", "canvas", "explain_web" |
| ] |
|
|
| TEMPLATES = { |
| "full_page": ["complete web page", "full html css javascript page", "entire website", "single page app"], |
| "landing_page": ["landing page", "marketing page", "startup page", "product page", "saas landing"], |
| "portfolio": ["portfolio", "personal website", "developer portfolio", "resume website"], |
| "navbar": ["navbar", "navigation bar", "responsive menu", "mobile menu", "hamburger menu"], |
| "hero": ["hero section", "main banner", "header section", "call to action"], |
| "responsive_grid": ["responsive grid", "css grid layout", "gallery grid", "cards grid"], |
| "card": ["card component", "pricing card", "profile card", "glass card"], |
| "form_validation": ["form validation", "validate form", "contact form", "email validation"], |
| "todo_app": ["todo app", "task list", "add remove tasks", "local storage todos"], |
| "dark_mode": ["dark mode", "theme toggle", "light dark theme", "css variables theme"], |
| "modal": ["modal", "popup", "dialog", "overlay"], |
| "tabs": ["tabs", "tab component", "tabbed interface"], |
| "accordion": ["accordion", "faq accordion", "collapsible sections"], |
| "carousel": ["carousel", "slider", "image slider"], |
| "css_animation": ["css animation", "advanced css", "keyframes", "animated button", "loading spinner"], |
| "dashboard": ["dashboard", "admin dashboard", "analytics layout", "sidebar dashboard"], |
| "fetch_api": ["fetch api", "load data from api", "javascript fetch", "async await web"], |
| "counter": ["counter app", "increment decrement", "button counter"], |
| "canvas": ["canvas", "draw with canvas", "html5 canvas"], |
| "explain_web": ["explain html", "explain css", "explain javascript", "what is flexbox", "what is css grid"], |
| } |
|
|
| PREFIXES = [ |
| "create", "build", "write", "make", "generate", "show me", "i need", |
| "dame", "crea", "haz", "construye", "escribe" |
| ] |
|
|
|
|
| def build_training_examples(mult: int = 45) -> List[Tuple[str, str]]: |
| examples: List[Tuple[str, str]] = [] |
| modifiers = [ |
| "responsive", "modern", "animated", "accessible", "mobile first", "with css variables", |
| "with flexbox", "with css grid", "with javascript", "dark theme", "glassmorphism", |
| "clean", "advanced css", "semantic html", "sin librerias", "vanilla javascript" |
| ] |
| for label, phrases in TEMPLATES.items(): |
| for phrase in phrases: |
| for pref in PREFIXES: |
| examples.append((f"{pref} {phrase}", label)) |
| for mod in modifiers[:8]: |
| examples.append((f"{pref} {mod} {phrase}", label)) |
| examples.append((f"{pref} {phrase} {mod}", label)) |
| return examples * mult |
|
|
|
|
| def html_shell(title: str, body: str, css: str, js: str = "") -> str: |
| return f'''<!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>{title}</title> |
| <style> |
| {css} |
| </style> |
| </head> |
| <body> |
| {body} |
| <script> |
| {js} |
| </script> |
| </body> |
| </html>''' |
|
|
| BASE_CSS = ''' :root { |
| --bg: #0f172a; |
| --panel: rgba(255,255,255,.08); |
| --text: #e5e7eb; |
| --muted: #94a3b8; |
| --brand: #7c3aed; |
| --brand-2: #06b6d4; |
| --ring: rgba(124,58,237,.45); |
| --radius: 22px; |
| --shadow: 0 24px 70px rgba(0,0,0,.35); |
| } |
| * { box-sizing: border-box; } |
| body { |
| margin: 0; |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; |
| background: radial-gradient(circle at top left, rgba(124,58,237,.25), transparent 30%), var(--bg); |
| color: var(--text); |
| min-height: 100vh; |
| } |
| a { color: inherit; text-decoration: none; } |
| button, input, textarea { font: inherit; } |
| ''' |
|
|
|
|
| def code_for(label: str, prompt: str) -> str: |
| p = prompt.lower() |
| if label in {"full_page", "landing_page", "portfolio"}: |
| title = "Portfolio" if label == "portfolio" or "portfolio" in p else "Nova Landing" |
| body = ''' <header class="nav"> |
| <a class="logo" href="#">Nova</a> |
| <nav> |
| <a href="#features">Features</a> |
| <a href="#work">Work</a> |
| <a href="#contact">Contact</a> |
| </nav> |
| <button id="themeBtn" class="ghost">Toggle theme</button> |
| </header> |
| |
| <main> |
| <section class="hero"> |
| <div class="hero-text"> |
| <p class="eyebrow">Modern Web Experience</p> |
| <h1>Build beautiful interfaces with HTML, CSS and JavaScript.</h1> |
| <p class="lead">Responsive layout, animated gradient, glass cards, semantic HTML and a tiny vanilla JS theme switcher.</p> |
| <div class="actions"> |
| <a class="btn" href="#contact">Start now</a> |
| <a class="btn secondary" href="#features">See features</a> |
| </div> |
| </div> |
| <div class="orb-card"> |
| <div class="orb"></div> |
| <h2>CSS Grid + Variables</h2> |
| <p>Fast, accessible, and easy to customize.</p> |
| </div> |
| </section> |
| |
| <section id="features" class="grid"> |
| <article class="card"><h3>Responsive</h3><p>Uses grid, clamp and media queries.</p></article> |
| <article class="card"><h3>Animated</h3><p>Subtle keyframes create depth.</p></article> |
| <article class="card"><h3>Vanilla JS</h3><p>No frameworks required.</p></article> |
| </section> |
| </main>''' |
| css = BASE_CSS + ''' |
| .nav { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:1.2rem clamp(1rem,4vw,4rem); position:sticky; top:0; backdrop-filter: blur(16px); background:rgba(15,23,42,.72); z-index:10; } |
| .logo { font-weight:900; letter-spacing:.04em; } |
| nav { display:flex; gap:1rem; color:var(--muted); } |
| .ghost { border:1px solid rgba(255,255,255,.16); background:transparent; color:var(--text); border-radius:999px; padding:.65rem 1rem; cursor:pointer; } |
| .hero { min-height:76vh; display:grid; grid-template-columns:1.1fr .9fr; align-items:center; gap:clamp(2rem,6vw,6rem); padding:clamp(2rem,6vw,6rem); } |
| .eyebrow { color:var(--brand-2); text-transform:uppercase; letter-spacing:.18em; font-size:.78rem; font-weight:800; } |
| h1 { font-size:clamp(2.4rem,7vw,6rem); line-height:.95; margin:.2em 0; } |
| .lead { color:var(--muted); font-size:clamp(1rem,2vw,1.25rem); max-width:65ch; } |
| .actions { display:flex; flex-wrap:wrap; gap:1rem; margin-top:2rem; } |
| .btn { background:linear-gradient(135deg,var(--brand),var(--brand-2)); padding:.9rem 1.2rem; border-radius:999px; font-weight:800; box-shadow:0 14px 35px var(--ring); } |
| .btn.secondary { background:rgba(255,255,255,.08); box-shadow:none; } |
| .orb-card { position:relative; overflow:hidden; border:1px solid rgba(255,255,255,.14); background:var(--panel); border-radius:var(--radius); padding:2rem; box-shadow:var(--shadow); min-height:360px; backdrop-filter:blur(20px); } |
| .orb { width:220px; height:220px; border-radius:50%; background:linear-gradient(135deg,var(--brand),var(--brand-2)); filter:blur(2px); animation:float 5s ease-in-out infinite; } |
| .grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1rem; padding:clamp(1rem,4vw,4rem); } |
| .card { border:1px solid rgba(255,255,255,.12); background:var(--panel); border-radius:var(--radius); padding:1.4rem; transition:transform .25s ease, border-color .25s ease; } |
| .card:hover { transform:translateY(-6px); border-color:var(--brand-2); } |
| body.light { --bg:#f8fafc; --panel:rgba(15,23,42,.06); --text:#0f172a; --muted:#475569; } |
| body.light .nav { background:rgba(248,250,252,.76); } |
| @keyframes float { 0%,100%{ transform:translateY(0) rotate(0deg);} 50%{ transform:translateY(-18px) rotate(8deg);} } |
| @media (max-width: 800px) { .hero { grid-template-columns:1fr; } .grid { grid-template-columns:1fr; } nav { display:none; } } |
| ''' |
| js = ''' const btn = document.querySelector('#themeBtn'); |
| btn.addEventListener('click', () => document.body.classList.toggle('light')); |
| ''' |
| return "```html\n" + html_shell(title, body, css, js) + "\n```" |
|
|
| if label == "navbar": |
| return '''```html |
| <header class="site-header"> |
| <a class="brand" href="#">Brand</a> |
| <button class="menu-btn" aria-expanded="false" aria-controls="nav">☰</button> |
| <nav id="nav" class="nav-links"> |
| <a href="#home">Home</a><a href="#about">About</a><a href="#work">Work</a><a href="#contact">Contact</a> |
| </nav> |
| </header> |
| <style> |
| .site-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 2rem;background:#0f172a;color:white;position:sticky;top:0}.brand{font-weight:900}.nav-links{display:flex;gap:1rem}.menu-btn{display:none}@media(max-width:700px){.menu-btn{display:block}.nav-links{display:none;position:absolute;top:100%;left:0;right:0;flex-direction:column;background:#111827;padding:1rem}.nav-links.open{display:flex}} |
| </style> |
| <script> |
| const btn=document.querySelector('.menu-btn'), nav=document.querySelector('#nav'); |
| btn.onclick=()=>{nav.classList.toggle('open');btn.setAttribute('aria-expanded',nav.classList.contains('open'));}; |
| </script> |
| ```''' |
|
|
| if label == "responsive_grid": |
| return '''```html |
| <section class="grid"> |
| <article>One</article><article>Two</article><article>Three</article><article>Four</article> |
| </section> |
| <style> |
| .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}.grid article{padding:1.5rem;border-radius:18px;background:linear-gradient(135deg,#1e293b,#334155);color:white;box-shadow:0 18px 45px rgba(15,23,42,.25)} |
| </style> |
| ```''' |
|
|
| if label == "form_validation": |
| return '''```html |
| <form id="contact" novalidate> |
| <label>Name <input name="name" required minlength="2"></label> |
| <label>Email <input name="email" required type="email"></label> |
| <button>Send</button> |
| <p id="msg" role="alert"></p> |
| </form> |
| <script> |
| const form=document.querySelector('#contact'), msg=document.querySelector('#msg'); |
| form.addEventListener('submit', e=>{ |
| e.preventDefault(); |
| if(!form.checkValidity()){ msg.textContent='Please complete the form correctly.'; return; } |
| msg.textContent='Message ready to send!'; |
| }); |
| </script> |
| ```''' |
|
|
| if label == "todo_app": |
| return '''```html |
| <div class="todo"> |
| <input id="task" placeholder="New task"><button id="add">Add</button><ul id="list"></ul> |
| </div> |
| <script> |
| const input=document.querySelector('#task'), list=document.querySelector('#list'); |
| const tasks=JSON.parse(localStorage.tasks||'[]'); |
| function render(){list.innerHTML='';tasks.forEach((t,i)=>{const li=document.createElement('li');li.innerHTML=`<span>${t}</span> <button data-i="${i}">Done</button>`;list.append(li);});localStorage.tasks=JSON.stringify(tasks)} |
| document.querySelector('#add').onclick=()=>{if(input.value.trim()){tasks.push(input.value.trim());input.value='';render();}}; |
| list.onclick=e=>{if(e.target.dataset.i){tasks.splice(+e.target.dataset.i,1);render();}}; |
| render(); |
| </script> |
| ```''' |
|
|
| if label == "modal": |
| return '''```html |
| <button id="open">Open modal</button> |
| <div class="modal" hidden><div class="box"><button id="close">×</button><h2>Hello</h2><p>This is an accessible modal pattern.</p></div></div> |
| <style>.modal{position:fixed;inset:0;background:rgba(0,0,0,.55);display:grid;place-items:center}.box{background:white;color:#111;padding:2rem;border-radius:18px;max-width:420px}</style> |
| <script> |
| const modal=document.querySelector('.modal');open.onclick=()=>modal.hidden=false;close.onclick=()=>modal.hidden=true;modal.onclick=e=>{if(e.target===modal)modal.hidden=true}; |
| </script> |
| ```''' |
|
|
| if label == "tabs": |
| return '''```html |
| <div class="tabs"><button data-tab="one">One</button><button data-tab="two">Two</button></div> |
| <section id="one">First panel</section><section id="two" hidden>Second panel</section> |
| <script> |
| document.querySelector('.tabs').onclick=e=>{if(!e.target.dataset.tab)return;document.querySelectorAll('section').forEach(s=>s.hidden=true);document.getElementById(e.target.dataset.tab).hidden=false;}; |
| </script> |
| ```''' |
|
|
| if label == "accordion": |
| return '''```html |
| <details open><summary>What is HTML?</summary><p>Semantic structure for web content.</p></details> |
| <details><summary>What is CSS?</summary><p>Presentation, layout and animation.</p></details> |
| <style>details{padding:1rem;margin:.5rem 0;border:1px solid #ddd;border-radius:12px}summary{cursor:pointer;font-weight:700}</style> |
| ```''' |
|
|
| if label == "css_animation": |
| return '''```html |
| <button class="glow">Hover me</button><div class="loader"></div> |
| <style> |
| .glow{padding:1rem 1.4rem;border:0;border-radius:999px;color:white;background:linear-gradient(135deg,#7c3aed,#06b6d4);transition:transform .25s, box-shadow .25s}.glow:hover{transform:translateY(-4px);box-shadow:0 18px 40px rgba(124,58,237,.45)} |
| .loader{width:56px;height:56px;border-radius:50%;border:6px solid #ddd;border-top-color:#7c3aed;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}} |
| </style> |
| ```''' |
|
|
| if label == "fetch_api": |
| return '''```html |
| <button id="load">Load user</button><pre id="out"></pre> |
| <script> |
| document.querySelector('#load').onclick=async()=>{ |
| const res=await fetch('https://jsonplaceholder.typicode.com/users/1'); |
| const data=await res.json(); |
| document.querySelector('#out').textContent=JSON.stringify(data,null,2); |
| }; |
| </script> |
| ```''' |
|
|
| if label == "counter": |
| return '''```html |
| <button id="dec">-</button><strong id="n">0</strong><button id="inc">+</button> |
| <script> |
| let n=0;const out=document.querySelector('#n'); |
| inc.onclick=()=>{n++;out.textContent=n};dec.onclick=()=>{n--;out.textContent=n}; |
| </script> |
| ```''' |
|
|
| if label == "canvas": |
| return '''```html |
| <canvas id="c" width="400" height="220"></canvas> |
| <script> |
| const ctx=document.querySelector('#c').getContext('2d'); |
| ctx.fillStyle='#0f172a';ctx.fillRect(0,0,400,220); |
| ctx.fillStyle='#06b6d4';ctx.beginPath();ctx.arc(200,110,70,0,Math.PI*2);ctx.fill(); |
| ctx.fillStyle='white';ctx.font='24px sans-serif';ctx.fillText('Canvas',158,118); |
| </script> |
| ```''' |
|
|
| if label == "explain_web": |
| return "HTML gives semantic structure, CSS controls layout/visual design, and JavaScript adds behavior. Advanced CSS includes Grid, Flexbox, custom properties, clamp(), container/media queries, transforms, transitions, keyframes, and accessibility-aware responsive design." |
|
|
| |
| return '''```html |
| <div class="card"> |
| <h2>Modern Card</h2> |
| <p>Reusable component with advanced CSS variables and hover motion.</p> |
| </div> |
| <style> |
| :root{--brand:#7c3aed;--bg:#0f172a;--text:#e5e7eb}.card{max-width:360px;padding:1.5rem;border-radius:22px;color:var(--text);background:linear-gradient(135deg,rgba(124,58,237,.25),rgba(6,182,212,.14)),var(--bg);box-shadow:0 24px 70px rgba(0,0,0,.28);transition:transform .25s}.card:hover{transform:translateY(-8px)} |
| </style> |
| ```''' |
|
|
|
|
| def train(out: Path): |
| out.mkdir(parents=True, exist_ok=True) |
| examples = build_training_examples(mult=35) |
| model = NBIntent(); model.fit(examples); model.save(out / 'web_intent_nb.json') |
| report = {"examples": len(examples), "features": len(model.vocab), "labels": WEB_LABELS, "type": "char_ngram_web_intent_plus_compositional_generator"} |
| (out / 'report.json').write_text(json.dumps(report, indent=2), encoding='utf-8') |
| (out / 'training_examples_sample.json').write_text(json.dumps(examples[:2000], indent=2, ensure_ascii=False), encoding='utf-8') |
| print(json.dumps(report, indent=2)) |
|
|
|
|
| def ask(out: Path, prompt: str): |
| model = NBIntent.load(out / 'web_intent_nb.json') |
| probs = model.predict_proba(prompt) |
| label = probs[0][0] |
| print('## Learned web reasoning') |
| print('- Read request with character n-grams and web-development fragments.') |
| print('- Top intents: ' + ', '.join(f'{l}={p:.2f}' for l,p in probs[:4])) |
| print(f'- Selected intent: {label}') |
| print('\n## Answer') |
| print(code_for(label, prompt)) |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument('--mode', choices=['train','ask'], default='ask') |
| ap.add_argument('--out', default='outputs/real_web_learner') |
| ap.add_argument('--prompt', default='create a responsive landing page with dark mode') |
| args = ap.parse_args(); out = Path(args.out) |
| if args.mode == 'train': train(out) |
| else: ask(out, args.prompt) |
|
|
| if __name__ == '__main__': |
| main() |
|
|