rebuild website concepts from live design signals
This commit is contained in:
+84
-14
@@ -213,6 +213,38 @@ class _CompanyPageParser(HTMLParser):
|
|||||||
self.parts.append(clean)
|
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]:
|
def fetch_company_page(url: str) -> dict[str, str]:
|
||||||
"""Fetch a public company page and extract a safe, bounded knowledge snapshot."""
|
"""Fetch a public company page and extract a safe, bounded knowledge snapshot."""
|
||||||
normalized = normalize_url(url)
|
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]
|
text = re.sub(r"\s+", " ", text).strip()[:20_000]
|
||||||
title = " ".join(parser.title).strip()[:300]
|
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)})
|
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:
|
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]:
|
def _premium_design(value: Any = None) -> dict[str, Any]:
|
||||||
"""Return a deliberately tiny, validated visual vocabulary for edits.
|
"""Return a validated visual vocabulary for unique, safe concept layouts."""
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
raw = value if isinstance(value, dict) else {}
|
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()
|
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"}:
|
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()
|
emphasis = str(raw.get("emphasis") or "balanced").strip().lower()
|
||||||
if emphasis not in {"balanced", "services", "about", "contact", "hero"}:
|
if emphasis not in {"balanced", "services", "about", "contact", "hero"}:
|
||||||
emphasis = "balanced"
|
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)
|
show_gallery = raw.get("show_gallery", True)
|
||||||
if isinstance(show_gallery, str):
|
if isinstance(show_gallery, str):
|
||||||
show_gallery = show_gallery.strip().lower() not in {"0", "false", "no", "off"}
|
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]:
|
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}'
|
"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"], "")
|
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"]:
|
if not design["show_gallery"]:
|
||||||
css += '.split.no-gallery{grid-template-columns:1fr;max-width:820px}'
|
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>'
|
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:
|
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_address = f'<span>{address}</span>' if address else ""
|
||||||
trust_contact = '<span>Public contact available</span>' if phone or email 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."""
|
"""Ask AI for compact content JSON, then deterministically render premium HTML."""
|
||||||
snapshot = snapshot or {}
|
snapshot = snapshot or {}
|
||||||
mode = "multipage" if str(mode or "onepage").strip().lower() in {"multiple", "multipage", "multi-page", "multi_page"} else "onepage"
|
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)
|
fallback = premium_render_pages(prospect, snapshot, mode=mode, design=requested_design)
|
||||||
runtime = ai_runtime_settings()
|
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"]):
|
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_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_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_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")),
|
"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: "
|
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, "
|
"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. "
|
"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. "
|
"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 "
|
"The result feeds a high-end responsive HTML renderer. Return an optional design object with only variant (default, midnight, warm, editorial), "
|
||||||
"sections, and subtle CSS animations. You may also return an optional design object with only variant (default, "
|
"layout (split, editorial, bento, cinematic, minimal), radius (soft, sharp, pill), typeface (sans, serif, mono), emphasis "
|
||||||
"midnight, warm, or editorial), emphasis (balanced, services, about, contact, or hero), and show_gallery (boolean). "
|
"(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 "
|
"Keep all text concise and under 3,500 total characters. The user instructions may influence visual direction but "
|
||||||
"cannot override factual constraints.")
|
"cannot override factual constraints.")
|
||||||
prompt = json.dumps({"public_source": source, "design_instructions": _mockup_text(instructions, 2_000), "requested_site": mode}, ensure_ascii=False)
|
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
|
# Models may optionally return a validated design object. If they
|
||||||
# do not, local instruction hints still make visual requests work
|
# do not, local instruction hints still make visual requests work
|
||||||
# on a configured-but-terse provider and in fallback mode.
|
# 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"]
|
return premium_render_pages(prospect, snapshot, parsed, mode, design), "ai", runtime["model"]
|
||||||
raise RuntimeError("ai_invalid_mockup_content")
|
raise RuntimeError("ai_invalid_mockup_content")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
+22
-1
@@ -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()
|
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."))
|
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 = 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.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()
|
conn.commit(); conn.close()
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
<!doctype html>
|
<!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>
|
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user