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: