rebuild website concepts from live design signals

This commit is contained in:
Marco0300
2026-09-14 16:33:32 +02:00
parent af7cbc6802
commit 286673eb1d
3 changed files with 107 additions and 16 deletions
+84 -14
View File
@@ -213,6 +213,38 @@ class _CompanyPageParser(HTMLParser):
self.parts.append(clean)
def _website_design_brief(markup: str) -> str:
"""Extract bounded visual/structural signals from the public page for redesign."""
raw = str(markup or "")[:1_000_000]
tags = re.findall(r"<([a-z][a-z0-9]*)\b", raw, flags=re.I)
counts = {tag: tags.count(tag) for tag in ("header", "nav", "main", "section", "article", "footer", "h1", "h2", "h3", "img", "button", "form") if tag in tags}
classes = re.findall(r"\bclass\s*=\s*['\"]([^'\"]+)['\"]", raw, flags=re.I)
class_tokens = sorted({token.lower() for value in classes for token in re.split(r"\s+", value) if token})[:80]
colors = sorted(set(re.findall(r"#[0-9a-fA-F]{3,8}\b", raw)))[:40]
fonts = sorted(set(re.findall(r"(?:font-family|font:)[^;{}]{0,120}", raw, flags=re.I)))[:20]
images = []
for value in re.findall(r"(?:src|data-src)\s*=\s*['\"]([^'\"]+)", raw, flags=re.I):
if value.startswith(("https://", "http://")) and value not in images:
images.append(value[:500])
links = []
for value in re.findall(r"href\s*=\s*['\"]([^'\"]+)", raw, flags=re.I):
if value.startswith(("/", "https://", "http://")) and value not in links:
links.append(value[:300])
headings = [re.sub(r"\s+", " ", value).strip()[:180] for value in re.findall(r"<h[1-3]\b[^>]*>(.*?)</h[1-3]>", raw, flags=re.I | re.S)]
signals = {
"tag_counts": counts,
"class_tokens": class_tokens,
"colors": colors,
"font_rules": fonts,
"image_urls": images[:20],
"navigation_links": links[:40],
"headings": [re.sub(r"<[^>]+>", "", value) for value in headings[:20]],
"has_dark_tokens": bool(re.search(r"\b(dark|black|navy|midnight)\b", " ".join(class_tokens), flags=re.I)),
"layout_tokens": [token for token in class_tokens if any(key in token for key in ("hero", "grid", "split", "card", "banner", "gallery", "masthead"))][:30],
}
return json.dumps(signals, ensure_ascii=False, separators=(",", ":"))[:12_000]
def fetch_company_page(url: str) -> dict[str, str]:
"""Fetch a public company page and extract a safe, bounded knowledge snapshot."""
normalized = normalize_url(url)
@@ -238,7 +270,7 @@ def fetch_company_page(url: str) -> dict[str, str]:
text = re.sub(r"\s+", " ", text).strip()[:20_000]
title = " ".join(parser.title).strip()[:300]
record_log("ai", "company_website_learned", details={"url": _safe_log_url(final_url), "title": title, "characters": len(text), "duration_ms": int((time.monotonic() - started) * 1000)})
return {"website_url": final_url, "website_title": title, "website_content": text, "website_fetched_at": now()}
return {"website_url": final_url, "website_title": title, "website_content": text, "website_design_brief": _website_design_brief(body.decode("utf-8", "replace")), "website_fetched_at": now()}
def _setting_int(saved: dict[str, str], key: str, env_key: str, default: int, minimum: int, maximum: int) -> int:
@@ -2903,12 +2935,7 @@ def _premium_content(prospect: dict[str, Any], snapshot: dict[str, Any] | None,
def _premium_design(value: Any = None) -> dict[str, Any]:
"""Return a deliberately tiny, validated visual vocabulary for edits.
The AI never gets to emit arbitrary CSS. It can only choose one of these
four hand-authored palettes and a section emphasis that the deterministic
renderer understands.
"""
"""Return a validated visual vocabulary for unique, safe concept layouts."""
raw = value if isinstance(value, dict) else {}
variant = str(raw.get("variant") or raw.get("theme") or raw.get("design_variant") or "default").strip().lower()
if variant not in {"default", "midnight", "warm", "editorial"}:
@@ -2916,10 +2943,37 @@ def _premium_design(value: Any = None) -> dict[str, Any]:
emphasis = str(raw.get("emphasis") or "balanced").strip().lower()
if emphasis not in {"balanced", "services", "about", "contact", "hero"}:
emphasis = "balanced"
layout = str(raw.get("layout") or "split").strip().lower()
if layout not in {"split", "editorial", "bento", "cinematic", "minimal"}:
layout = "split"
radius = str(raw.get("radius") or "soft").strip().lower()
if radius not in {"soft", "sharp", "pill"}:
radius = "soft"
typeface = str(raw.get("typeface") or "sans").strip().lower()
if typeface not in {"sans", "serif", "mono"}:
typeface = "sans"
show_gallery = raw.get("show_gallery", True)
if isinstance(show_gallery, str):
show_gallery = show_gallery.strip().lower() not in {"0", "false", "no", "off"}
return {"variant": variant, "emphasis": emphasis, "show_gallery": bool(show_gallery)}
return {"variant": variant, "emphasis": emphasis, "layout": layout, "radius": radius, "typeface": typeface, "show_gallery": bool(show_gallery)}
def _design_from_source(prospect: dict[str, Any], snapshot: dict[str, Any] | None, instructions: str = "") -> dict[str, Any]:
"""Choose a stable starting direction from the live site and business identity."""
snapshot = snapshot or {}
seed = "|".join(str(snapshot.get(key) or prospect.get(key) or "") for key in ("website_url", "website_design_brief", "name", "category"))
digest = hashlib.sha256(seed.encode("utf-8", "replace")).digest()
choices = {
"layout": ("split", "editorial", "bento", "cinematic", "minimal"),
"variant": ("default", "midnight", "warm", "editorial"),
"radius": ("soft", "sharp", "pill"),
"typeface": ("sans", "serif", "mono"),
}
design = {key: values[digest[index] % len(values)] for index, (key, values) in enumerate(choices.items())}
brief = str(snapshot.get("website_design_brief") or "").lower()
if '"has_dark_tokens":true' in brief or re.search(r"\b(dark|black|navy|midnight)\b", brief):
design["variant"] = "midnight"
return _premium_design_from_instructions(instructions, design)
def _premium_design_from_instructions(instructions: str, current: Any = None) -> dict[str, Any]:
@@ -2988,6 +3042,17 @@ def premium_render_pages(prospect: dict[str, Any], snapshot: dict[str, Any] | No
"editorial": ':root{--accent:#a95643;--accent-2:#386f69;--paper:#faf8f3;--card:#fff;--line:#e5e0d6;--ink:#211c1b;--ink-soft:#5e5550;--muted:#867d76;font-family:Georgia,"Times New Roman",serif}.nav,.btn,.eyebrow,.service-index,.preview,.footer-grid h3,.footer-bottom{font-family:Inter,ui-sans-serif,system-ui,sans-serif}.hero{background:linear-gradient(135deg,#f1ece4 0,#faf8f3 58%,#e9f2ee 100%)}.service-card{border-radius:8px}.cta-box{border-radius:8px}'
}
css += palette_overrides.get(design["variant"], "")
layout_overrides = {
"editorial": '.layout-editorial .hero-grid{grid-template-columns:.78fr 1.22fr;gap:92px}.layout-editorial .hero h1{font-family:Georgia,"Times New Roman",serif;font-weight:500;letter-spacing:-.055em}.layout-editorial .service-grid{grid-template-columns:1fr 1fr}.layout-editorial .service-card:nth-child(1){grid-column:span 2}',
"bento": '.layout-bento .hero-grid{grid-template-columns:1fr;max-width:980px}.layout-bento .hero-visual{max-width:760px;width:100%;margin-left:auto}.layout-bento .service-grid{grid-template-columns:1.4fr .8fr .8fr}.layout-bento .service-card:nth-child(1){min-height:310px}',
"cinematic": '.layout-cinematic .hero{background:var(--dark);color:#fff}.layout-cinematic .hero-copy,.layout-cinematic .trust-rail{color:#c4d0e1}.layout-cinematic .hero-grid{min-height:700px}.layout-cinematic .hero-visual{transform:scale(1.08)}',
"minimal": '.layout-minimal .hero-grid{grid-template-columns:1fr;max-width:900px}.layout-minimal .hero-visual{display:none}.layout-minimal .service-grid{grid-template-columns:1fr;border-top:1px solid var(--line)}.layout-minimal .service-card{border-width:0 0 1px;border-radius:0;box-shadow:none;min-height:150px}',
}
css += layout_overrides.get(design["layout"], "")
radius_overrides = {"sharp": '.radius-sharp .btn,.radius-sharp .service-card,.radius-sharp .cta-box,.radius-sharp .image-shell,.radius-sharp .split-image{border-radius:3px}.radius-sharp .brand-mark{border-radius:0}', "pill": '.radius-pill .btn,.radius-pill .contact-chip,.radius-pill .service-card{border-radius:999px}.radius-pill .service-card{padding:28px 30px}'}
css += radius_overrides.get(design["radius"], "")
type_overrides = {"serif": '.type-serif,.type-serif .nav,.type-serif .btn{font-family:Georgia,"Times New Roman",serif}.type-serif .navlinks,.type-serif .eyebrow{font-family:Inter,ui-sans-serif,sans-serif}', "mono": '.type-mono,.type-mono .nav,.type-mono .btn{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.type-mono h1,.type-mono h2,.type-mono h3{letter-spacing:-.04em}'}
css += type_overrides.get(design["typeface"], "")
if not design["show_gallery"]:
css += '.split.no-gallery{grid-template-columns:1fr;max-width:820px}'
@@ -3021,7 +3086,7 @@ def premium_render_pages(prospect: dict[str, Any], snapshot: dict[str, Any] | No
return f'<footer class="site-footer"><div class="footer-grid"><div><a class="brand footer-brand" href="{home_href}"><i class="brand-mark"></i>{name}</a><p>{html_escape(_mockup_text(content["intro"], 220))}</p></div><div><h3>Explore</h3><a href="{services_href}">Services</a><a href="{about_href}">About</a><a href="{contact_href}">Contact</a></div><div><h3>Contact</h3>{contact or contact_fallback}</div></div><div class="footer-bottom"><span>{name} · website concept preview</span><span>Designed as a mobile-first concept</span></div></footer>'
def shell(slug: str, page_title: str, body: str) -> str:
return f'<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0c1423"><title>{name} · {html_escape(page_title)}</title><style>{css}</style></head><body class="variant-{design["variant"]} emphasis-{design["emphasis"]}"><div class="preview">Website concept preview · prepared for review</div>{nav_html(slug)}<main>{body}</main>{footer_html()}</body></html>'
return f'<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0c1423"><title>{name} · {html_escape(page_title)}</title><style>{css}</style></head><body class="variant-{design["variant"]} emphasis-{design["emphasis"]} layout-{design["layout"]} radius-{design["radius"]} type-{design["typeface"]}"><div class="preview">Website concept preview · prepared for review</div>{nav_html(slug)}<main>{body}</main>{footer_html()}</body></html>'
trust_address = f'<span>{address}</span>' if address else ""
trust_contact = '<span>Public contact available</span>' if phone or email else ""
@@ -3048,7 +3113,7 @@ def generate_premium_mockup(prospect: dict[str, Any], snapshot: dict[str, Any] |
"""Ask AI for compact content JSON, then deterministically render premium HTML."""
snapshot = snapshot or {}
mode = "multipage" if str(mode or "onepage").strip().lower() in {"multiple", "multipage", "multi-page", "multi_page"} else "onepage"
requested_design = _premium_design_from_instructions(instructions)
requested_design = _design_from_source(prospect, snapshot, instructions)
fallback = premium_render_pages(prospect, snapshot, mode=mode, design=requested_design)
runtime = ai_runtime_settings()
if not runtime["base_url"] or not runtime["model"] or (ai_requires_api_key(runtime["provider"]) and not runtime["api_key"]):
@@ -3062,6 +3127,8 @@ def generate_premium_mockup(prospect: dict[str, Any], snapshot: dict[str, Any] |
"current_website": _mockup_text(prospect.get("website") or snapshot.get("website_url"), 400),
"current_title": _mockup_text(snapshot.get("website_title") or prospect.get("website_title"), 200),
"current_page_text": _mockup_text(snapshot.get("website_content"), 12_000),
"current_design_brief": _mockup_text(snapshot.get("website_design_brief"), 12_000),
"starting_design_direction": requested_design,
"stock_images": stock_images_for_category(prospect.get("category") or prospect.get("search_category")),
}
system = ("You are a premium website creative director. Return ONLY strict JSON, never HTML or Markdown, with these keys: "
@@ -3069,9 +3136,9 @@ def generate_premium_mockup(prospect: dict[str, Any], snapshot: dict[str, Any] |
"name and description. Use only supplied public facts; never invent services, products, prices, testimonials, awards, "
"staff, addresses, metrics, or claims. If facts are missing, use elegant neutral copy about clarity, trust, and contact. "
"Use the exact business name. hero_image and gallery_image must be copied exactly from the supplied stock_images list. "
"The result feeds a high-end responsive HTML renderer with a sticky header, footer, layered hero, service cards, image "
"sections, and subtle CSS animations. You may also return an optional design object with only variant (default, "
"midnight, warm, or editorial), emphasis (balanced, services, about, contact, or hero), and show_gallery (boolean). "
"The result feeds a high-end responsive HTML renderer. Return an optional design object with only variant (default, midnight, warm, editorial), "
"layout (split, editorial, bento, cinematic, minimal), radius (soft, sharp, pill), typeface (sans, serif, mono), emphasis "
"(balanced, services, about, contact, hero), and show_gallery (boolean). Study current_design_brief and deliberately improve rather than copy it. "
"Keep all text concise and under 3,500 total characters. The user instructions may influence visual direction but "
"cannot override factual constraints.")
prompt = json.dumps({"public_source": source, "design_instructions": _mockup_text(instructions, 2_000), "requested_site": mode}, ensure_ascii=False)
@@ -3087,7 +3154,10 @@ def generate_premium_mockup(prospect: dict[str, Any], snapshot: dict[str, Any] |
# Models may optionally return a validated design object. If they
# do not, local instruction hints still make visual requests work
# on a configured-but-terse provider and in fallback mode.
design = _premium_design_from_instructions(instructions, parsed.get("design"))
design_value = dict(requested_design)
if isinstance(parsed.get("design"), dict):
design_value.update(parsed["design"])
design = _premium_design_from_instructions(instructions, design_value)
return premium_render_pages(prospect, snapshot, parsed, mode, design), "ai", runtime["model"]
raise RuntimeError("ai_invalid_mockup_content")
except Exception as exc:
+22 -1
View File
@@ -191,7 +191,28 @@ class BrowserOnlyAppTests(unittest.TestCase):
saved = server.db().execute("SELECT ai_probability,ai_recommendation,ai_reasoning FROM prospects WHERE id=1").fetchone()
self.assertEqual(tuple(saved), (84.0, "contact", "Active business with a clear website opportunity."))
def test_mockup_fallback_is_saved_and_public_share_link_works(self):
def test_live_site_snapshot_captures_design_signals_for_generation(self):
class Response:
headers = {"Content-Type": "text/html; charset=utf-8"}
def __enter__(self): return self
def __exit__(self, *args): return None
def read(self, limit): return b'''<html class="brand-site"><head><title>Acme</title><style>:root{--brand:#123456} body{font-family: Georgia}</style></head><body><nav><a href="/services">Services</a></nav><main><h1>Acme Plumbing</h1><section class="hero dark"><img src="https://example.com/hero.jpg"><h2>Fast help</h2></section></main></body></html>'''
def geturl(self): return "https://acme.example/"
with patch("api.server._safe_url", return_value=True), patch("api.server.urllib.request.urlopen", return_value=Response()):
snapshot = server.fetch_company_page("https://acme.example/")
self.assertIn("hero", snapshot["website_design_brief"])
self.assertIn("#123456", snapshot["website_design_brief"])
self.assertIn("Georgia", snapshot["website_design_brief"])
self.assertIn("https://example.com/hero.jpg", snapshot["website_design_brief"])
def test_rendered_design_variants_are_not_the_same_template(self):
prospect = {"name": "Acme Plumbing", "category": "plumbers", "address": "Cape Town"}
first = server.premium_render_pages(prospect, {}, mode="onepage", design={"variant": "midnight", "layout": "editorial", "radius": "sharp"})["home"]
second = server.premium_render_pages(prospect, {}, mode="onepage", design={"variant": "warm", "layout": "bento", "radius": "soft"})["home"]
self.assertNotEqual(first, second)
self.assertIn("layout-editorial", first)
self.assertIn("layout-bento", second)
conn = server.db()
conn.execute("INSERT INTO prospects(name,domain,category,address,phone,emails,website,website_status,discovered_at) VALUES(?,?,?,?,?,?,?,?,?)", ("Acme Plumbing", "maps:acme", "plumbers", "Cape Town", "+27101234567", "info@acme.co.za", "", "no_detected_website", server.now()))
conn.commit(); conn.close()
+1 -1
View File
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0d1726"><title>ProspectOS</title><link rel="stylesheet" href="/style.css?v=workspace-v8"></head>
<body><div class="app-shell"><aside class="sidebar"><div class="brand"><span class="brand-mark">P</span><div><b>ProspectOS</b><small>Growth workspace</small></div></div><nav id="nav"><a href="#dashboard" data-route="dashboard"><span>⌂</span>Dashboard</a><a href="#discover" data-route="discover"><span>⌁</span>Discover</a><a href="#prospects" data-route="prospects"><span>▦</span>Prospects</a><a href="#runs" data-route="runs"><span>◷</span>Runs</a><a href="#settings" data-route="settings"><span>⚙</span>Settings</a></nav><div class="sidebar-foot"><span class="status-dot" id="sourceDot"></span><div><b id="sourceLabel">Checking worker</b><small id="sourceSub">Connecting…</small></div></div></aside><main class="main"><header class="topbar"><div><span class="crumb">Workspace</span><span class="slash">/</span><b id="pageName">Dashboard</b></div><div class="top-actions"><span id="updated">—</span><button class="icon-btn" id="refreshAll" title="Refresh">↻</button></div></header><div id="view"></div><footer>ProspectOS · public data only · manual review before contact</footer></main></div><aside id="detailDrawer" class="detail-drawer" aria-hidden="true"><div class="drawer-head"><div><span class="eyebrow">PROSPECT DETAIL</span><h2 id="drawerName">Prospect</h2></div><button class="icon-btn" id="closeDrawer">×</button></div><div id="drawerBody"></div></aside><dialog id="messageDialog"><form method="dialog" id="messageForm"><div class="dialog-head"><div><label class="eyebrow">CLIENT-SPECIFIC DRAFT</label><h2 id="messageTitle">Write a message for this prospect</h2><p>Use verified facts and your offer to create an editable draft.</p></div><button class="icon-btn" value="cancel">×</button></div><label for="offerInput">Your offer or instruction</label><textarea id="offerInput" rows="4" placeholder="Move hosting to Gold Plus for R119/month and rebuild the website free."></textarea><div id="messageStatus" class="notice"></div><button class="primary" id="generateMessage" type="button">Generate client-specific draft</button><label for="subjectOutput">Subject</label><input id="subjectOutput"><label for="bodyOutput">Email draft</label><textarea id="bodyOutput" rows="12"></textarea><div class="dialog-actions"><button class="secondary" id="copyMessage" type="button">Copy draft</button><button class="secondary" value="cancel">Close</button></div></form></dialog><script src="/app.js?v=workspace-v8"></script></body></html>
<body><div class="app-shell"><aside class="sidebar"><div class="brand"><span class="brand-mark">P</span><div><b>ProspectOS</b><small>Growth workspace</small></div></div><nav id="nav"><a href="#dashboard" data-route="dashboard"><span>⌂</span>Dashboard</a><a href="#discover" data-route="discover"><span>⌁</span>Discover</a><a href="#prospects" data-route="prospects"><span>▦</span>Prospects</a><a href="#runs" data-route="runs"><span>◷</span>Runs</a><a href="#settings" data-route="settings"><span>⚙</span>Settings</a></nav><div class="sidebar-foot"><span class="status-dot" id="sourceDot"></span><div><b id="sourceLabel">Checking worker</b><small id="sourceSub">Connecting…</small></div></div></aside><main class="main"><header class="topbar"><div><span class="crumb">Workspace</span><span class="slash">/</span><b id="pageName">Dashboard</b></div><div class="top-actions"><span id="updated">—</span><button class="icon-btn" id="refreshAll" title="Refresh">↻</button></div></header><div id="view"></div><footer>ProspectOS · public data only · manual review before contact</footer></main></div><aside id="detailDrawer" class="detail-drawer" aria-hidden="true"><div class="drawer-head"><div><span class="eyebrow">PROSPECT DETAIL</span><h2 id="drawerName">Prospect</h2></div><button class="icon-btn" id="closeDrawer">×</button></div><div id="drawerBody"></div></aside><dialog id="messageDialog"><form method="dialog" id="messageForm"><div class="dialog-head"><div><label class="eyebrow">CLIENT-SPECIFIC DRAFT</label><h2 id="messageTitle">Write a message for this prospect</h2><p>Use verified facts and your offer to create an editable draft.</p></div><button class="icon-btn" value="cancel">×</button></div><label for="offerInput">Your offer or instruction</label><textarea id="offerInput" rows="4" placeholder="Move hosting to Gold Plus for R119/month and rebuild the website free."></textarea><div id="messageStatus" class="notice"></div><button class="primary" id="generateMessage" type="button">Generate client-specific draft</button><label for="subjectOutput">Subject</label><input id="subjectOutput"><label for="bodyOutput">Email draft</label><textarea id="bodyOutput" rows="12"></textarea><div class="dialog-actions"><button class="secondary" id="copyMessage" type="button">Copy draft</button><button class="secondary" value="cancel">Close</button></div></form></dialog><script src="/app.js?v=workspace-v9"></script></body></html>