from __future__ import annotations import json from abc import ABC, abstractmethod from core.config import MODEL_BACKEND from core.risks import HIGH_JARGON, NO_PRODUCT_VISUAL, PRICING_UNCLEAR from core.schemas import ( CTA, AggregatedReport, Archetype, ContentSection, HeuristicReport, Persona, RedesignSuggestion, UIAnalysis, VisualAssessment, ) class ModelClient(ABC): @abstractmethod def vision_analyze(self, image_path: str | None, app_description: str) -> UIAnalysis: ... @abstractmethod def customize_personas( self, archetypes: list[Archetype], app_description: str, target_audience: str, ) -> list[Persona]: ... @abstractmethod def run_swarm( self, personas: list[Persona], ui: UIAnalysis, heuristics: HeuristicReport, flow_steps: list[str], ) -> list[tuple[str, str]]: ... @abstractmethod def generate_redesign( self, report: AggregatedReport, ui: UIAnalysis, heuristics: HeuristicReport, ) -> RedesignSuggestion: ... class MockClient(ModelClient): def vision_analyze(self, image_path: str | None, app_description: str) -> UIAnalysis: text = app_description.lower() good = any(term in text for term in ["clear", "good", "simple", "trusted"]) if good: return UIAnalysis( headline="Ship customer onboarding in minutes", subheadline=( "Create guided product tours with templates, analytics, and a free trial." ), primary_cta=CTA( text="Start free trial", position="above_fold", visibility="prominent" ), secondary_ctas=["Watch demo"], navigation=["Product", "Pricing", "Customers", "Docs"], content_sections=[ ContentSection( type="product_visual", summary="Large dashboard screenshot above the fold" ), ContentSection(type="pricing", summary="Free trial and transparent paid tiers"), ContentSection( type="testimonials", summary="Customer logos and two short reviews" ), ], visual_assessment=VisualAssessment( clutter_level="low", mobile_friendliness="good", text_density="moderate", contrast_issues="none", ), overall_first_impression="Clear value proposition with a visible free-trial CTA.", ) return UIAnalysis( headline="Revolutionary agentic synergy for scalable workflow transformation", subheadline=( "Leverage our holistic ecosystem to optimize enterprise productivity instantly." ), primary_cta=CTA(text="Get Started", position="above_fold", visibility="subtle"), secondary_ctas=["Learn More", "Contact Sales"], navigation=["Platform", "Solutions", "Company"], content_sections=[ ContentSection(type="hero", summary="Text-heavy hero with abstract gradient art"), ContentSection(type="features", summary="Dense feature claims without screenshots"), ], visual_assessment=VisualAssessment( clutter_level="high", mobile_friendliness="poor", text_density="dense", contrast_issues="minor", ), overall_first_impression=( "Ambitious but vague; the page does not quickly explain the product." ), ) def customize_personas( self, archetypes: list[Archetype], app_description: str, target_audience: str, ) -> list[Persona]: personas: list[Persona] = [] for idx, archetype in enumerate(archetypes, start=1): personas.append( Persona( persona_id=f"P{idx:02d}", archetype=archetype.archetype, name=f"Persona {idx}", backstory=f"Evaluating this product for {target_audience or 'their team'}.", goal="Understand the product and decide whether to continue onboarding.", device="mobile_chrome" if "mobile" in archetype.archetype else "desktop_chrome", tolerance=archetype.key_trait, constraints=[archetype.key_trait], brutal_quote_style="direct, specific, first-person", ) ) return personas def run_swarm( self, personas: list[Persona], ui: UIAnalysis, heuristics: HeuristicReport, flow_steps: list[str], ) -> list[tuple[str, str]]: from agents.personas import ( archetype_is_vulnerable, issues_for_archetype, quote_for_archetype, ) total_steps = max(len(flow_steps), 3) raw_results: list[tuple[str, str]] = [] severe = bool( set(heuristics.risk_flags) & {HIGH_JARGON, PRICING_UNCLEAR, NO_PRODUCT_VISUAL} ) for persona in personas: fails = severe and archetype_is_vulnerable(persona.archetype, heuristics) final_step = 2 if fails else total_steps issues = issues_for_archetype(persona.archetype, heuristics) result = { "persona_id": persona.persona_id, "archetype": persona.archetype, "completed_task": not fails, "final_step_reached": final_step, "total_steps": total_steps, "step_log": [ { "step": 1, "action": "Read the landing page headline", "understanding": f"Saw headline: {ui.headline}", "confusion_level": 0.75 if "HIGH_JARGON" in heuristics.risk_flags else 0.2, "would_continue": True, "issues": [ issue for issue in issues if "jargon" in issue.lower() or "headline" in issue.lower() ], }, { "step": 2, "action": "Looked for the next onboarding action", "understanding": f"Primary CTA says '{ui.primary_cta.text}'.", "confusion_level": 0.85 if fails else 0.25, "would_continue": not fails, "issues": issues, }, ], "overall_confusion_score": 0.78 if fails else 0.24, "overall_trust_score": 0.34 if fails else 0.76, "brutal_quote": quote_for_archetype(persona.archetype, ui.primary_cta.text, fails), "top_issues": issues, "suggested_fix": ( "Use plain copy, show the product, and clarify pricing near the CTA." ) if fails else "Keep the clear CTA and supporting proof visible above the fold.", } raw_results.append((persona.persona_id, json.dumps(result))) return raw_results def generate_redesign( self, report: AggregatedReport, ui: UIAnalysis, heuristics: HeuristicReport, ) -> RedesignSuggestion: return RedesignSuggestion.model_validate( { "priority_fixes": [ { "issue": report.top_issues[0].issue if report.top_issues else "Clarify value proposition", "severity": "high", "affected_segments": [ segment.archetype for segment in report.worst_segments[:3] ], "current": ui.headline, "proposed_alternatives": [ "Understand your onboarding failures before launch", "See where new users get confused in 60 seconds", ], "reasoning": ( "The weakest segments need immediate clarity before " "they will trust the CTA." ), } ], "onboarding_flow_fix": { "current": [step.label for step in report.failure_funnel], "proposed": [ "Show concrete example", "Let user try", "Ask for signup after value", ], "reasoning": "Move proof and value before commitment.", }, "quick_wins": [ "Rewrite the headline in plain language under 8 words", "Add pricing or 'Free trial' copy next to the primary CTA", "Show a real product screenshot above the fold", "Reduce competing CTAs to one primary and one secondary action", ], } ) class VLLMClient(MockClient): """Placeholder preserving the backend boundary until AMD/vLLM infra is available.""" class HFInferenceClient(MockClient): """Placeholder preserving the backend boundary until HF Inference is wired.""" def get_model_client() -> ModelClient: if MODEL_BACKEND == "vllm": return VLLMClient() if MODEL_BACKEND == "hf_api": return HFInferenceClient() return MockClient()