# Keak Docs Full-text index of all documentation pages, intended for LLM consumption. /docs/ai-infrastructure/database/telemetry/keak-code # Keak-Code Telemetry DB The Keak-Code IDE tracks every AI generation session to measure variant quality, understand user behavior, and link generated variants to production experiments. This document explains how data flows through the system. ## System Architecture ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ KEAK-CODE IDE (Electron) │ │ ┌─────────────────────────┐ ┌──────────────────────────────────────┐ │ │ │ use-cro-telemetry.ts │ │ use-keafab-generator.ts │ │ │ │ (drawer open/close) │ │ (generation lifecycle) │ │ │ └───────────┬─────────────┘ └───────────────────┬──────────────────┘ │ │ │ │ │ │ └──────────────────┬───────────────────┘ │ │ ▼ │ │ ┌─────────────────────────┐ │ │ │ telemetryService.ts │ │ │ └───────────┬─────────────┘ │ └────────────────────────────────┼────────────────────────────────────────────┘ │ HTTP ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ DASHBOARD (Next.js) │ │ /app/api/telemetry/* endpoints │ └────────────────────────────────┼────────────────────────────────────────────┘ │ Supabase Client ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ TELEMETRY DATABASE (Supabase) │ │ Separate instance from Dashboard database │ │ (TELEMETRY_SUPABASE_URL) │ └─────────────────────────────────────────────────────────────────────────────┘ ``` The telemetry database is a **dedicated Supabase instance**, separate from the main Dashboard database. The API endpoints live in the Dashboard codebase (`/app/api/telemetry/*`), but write to this separate database via `TELEMETRY_SUPABASE_URL`. ## The User Journey ### 1. User Clicks an Element When a user clicks an element in the preview to open the CRO drawer, the `use-cro-telemetry.ts` hook initializes telemetry: ``` User clicks element │ ▼ ┌─────────────────────────────────┐ │ startSession(projectId) │ → Creates session record (or reuses existing) └─────────────────┬───────────────┘ ▼ ┌─────────────────────────────────┐ │ lookupChat(projectId, keakId) │ → Check if conversation exists for this element └─────────────────┬───────────────┘ │ ┌───────┴───────┐ │ │ Found Not Found │ │ Resume trackChatStart() │ │ │ ├── element_selector │ ├── original_code │ └── source (file context) │ │ └───────┬───────┘ ▼ setChatId() → Ready for generation ``` **Key behavior**: Chats use UPSERT on `(project_id, keak_id)`. The same element always resumes the same conversation, even across different sessions. ### 2. User Submits a Prompt When the user submits a prompt to generate a variant, `use-keafab-generator.ts` orchestrates the telemetry: ``` User submits prompt │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Determine generation_type: │ │ • INITIAL → first prompt in conversation │ │ • RETRY → user clicked retry button │ │ • FOLLOWUP → user submitted follow-up instructions │ └─────────────────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ If previous variant exists, mark its outcome: │ │ • Retry → trackVariantRetry() → REPLACED │ │ • Followup → trackVariantFollowup() → SKIPPED │ └─────────────────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ trackGenerationStart() │ │ Records BEFORE the LLM call: │ │ • chatId, sessionId │ │ • userPrompt, generationType │ │ • screenshotUrl, mediaUrls │ └─────────────────────────────────┬───────────────────────┘ │ ▼ Call KeaFab API │ ▼ ┌─────────────────────────────────────────────────────────┐ │ trackGenerationComplete() │ │ Records AFTER the LLM responds: │ │ • durationMs, inputTokens, outputTokens │ │ • llm_response (analysis + changes) │ │ AND creates a new variant record │ └─────────────────────────────────────────────────────────┘ ``` **Why two phases?** Recording start time separately allows accurate duration measurement even if the LLM call fails. ### 3. User Previews the Variant Each time the generated code is rendered in the preview iframe: ``` trackVariantPreview(variantId, newCode) │ ├── Increment preview_count ├── Set first_previewed_at (if first time) ├── Update last_previewed_at │ └── Compare newCode vs original variant_code │ ┌───────┴───────┐ │ │ Same Different │ │ Stay PENDING Set EDITED has_edits: true final_code: newCode ``` **Edit detection**: The system automatically detects when users modify the generated code before accepting. ### 4. User Takes Final Action ``` ┌──────────────────┬────────────────────┬─────────────────────────┐ │ Action │ Endpoint │ Outcome Set │ ├──────────────────┼────────────────────┼─────────────────────────┤ │ Accept variant │ /variants/accept │ ACCEPTED │ │ Give feedback │ /variants/feedback │ (sets feedback field) │ │ Clear chat │ /chats/clear │ CLEARED (non-terminal) │ └──────────────────┴────────────────────┴─────────────────────────┘ ``` Note: `/chats/clear` only affects variants NOT already in `REPLACED` or `ACCEPTED` states. ### 5. Launch as Experiment (Optional) When a variant is deployed as a Dashboard A/B test: ```typescript telemetry.linkVariantToExperiment(variantId, experimentId) ``` This sets `experiment_id` on the variant, linking it to the Dashboard `tests` table. ## Variant Lifecycle State Machine ``` ┌──────────────┐ │ PENDING │ ← Created on generation complete └──────┬───────┘ │ ┌──────────────────────┼──────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ EDITED │ │ ACCEPTED │ │ REPLACED │ │ (preview with │ │ (user applied │ │ (user clicked │ │ code edits) │ │ to codebase) │ │ retry) │ └────────┬────────┘ └─────────────────┘ └─────────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ ACCEPTED │ │ SKIPPED │ │ (accept after │ │ (followup) │ │ editing) │ └─────────────────┘ └─────────────────┘ ┌─────────────────┐ │ CLEARED │ ← Chat cleared (only for PENDING/EDITED) └─────────────────┘ ``` ## Database Schema ### sessions Tracks work periods within the IDE. | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Primary key | | `project_id` | uuid | Associated project | | `user_id` | uuid | Authenticated user | | `created_at` | timestamptz | Session start | ### chats Element-specific conversations, keyed by `(project_id, keak_id)`. | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Primary key | | `project_id` | uuid | Associated project | | `keak_id` | text | Element identifier (injected into DOM) | | `element_selector` | text | CSS selector for the element | | `original_code` | text | Original element HTML/JSX | | `created_at` | timestamptz | Chat creation | **Unique constraint**: `(project_id, keak_id)` - enables UPSERT pattern. ### source\_code File context snapshots, deduplicated by content. | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Primary key | | `chat_id` | uuid | FK → chats.id | | `path` | text | File path in project | | `code` | text | Full source content | | `created_at` | timestamptz | Capture time | ### generations LLM request records with two-phase lifecycle. | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Primary key | | `chat_id` | uuid | FK → chats.id | | `session_id` | uuid | FK → sessions.id | | `user_prompt` | text | User's instruction | | `generation_type` | enum | INITIAL, RETRY, FOLLOWUP | | `llm_model` | text | Model identifier | | `model_tier` | text | Pricing tier | | `screenshot_url` | text | Captured screenshot | | `media` | text\[] | Additional media URLs | | `duration_ms` | integer | Generation time (set on complete) | | `input_tokens` | integer | Prompt tokens (set on complete) | | `output_tokens` | integer | Response tokens (set on complete) | | `llm_response` | text | Raw response (set on complete) | | `created_at` | timestamptz | Start time | ### variants Generated code with outcome tracking. | Column | Type | Description | |--------|------|-------------| | `id` | uuid | Primary key | | `generation_id` | uuid | FK → generations.id | | `chat_id` | uuid | FK → chats.id | | `experiment_id` | integer | FK → Dashboard tests.id | | `variant_code` | text | Original generated code | | `final_code` | text | Code after user edits | | `has_edits` | boolean | Whether user modified code | | `validation_score` | integer | Code validation score | | `outcome` | enum | PENDING, EDITED, ACCEPTED, REPLACED, CLEARED, SKIPPED | | `feedback` | enum | LIKE, DISLIKE, or null | | `preview_count` | integer | Number of previews | | `first_previewed_at` | timestamptz | First preview | | `last_previewed_at` | timestamptz | Last preview | | `created_at` | timestamptz | Creation time | | `updated_at` | timestamptz | Last update | ## API Reference All endpoints require authentication and are prefixed with `/api/telemetry`. **Client**: These endpoints are called by `telemetryService.ts` in the Keak-Code IDE (`keak-code/src/renderer/src/services/telemetryService.ts`). ### Sessions | Endpoint | Method | Description | |----------|--------|-------------| | `/sessions/create` | POST | Create session for project | ### Chats | Endpoint | Method | Description | |----------|--------|-------------| | `/chats/lookup` | GET | Find chat by `projectId` + `keakId` | | `/chats/start` | POST | Create/upsert chat with element context | | `/chats/clear` | POST | Mark non-terminal variants as CLEARED | ### Generations | Endpoint | Method | Description | |----------|--------|-------------| | `/generations/start` | POST | Record generation start (returns `generationId`) | | `/generations/complete` | POST | Update metrics + create variant (returns `variantId`) | ### Variants | Endpoint | Method | Description | |----------|--------|-------------| | `/variants/lookup` | GET | Get latest variant for chat | | `/variants/update` | PATCH | Generic field update | | `/variants/preview` | POST | Track preview + detect edits | | `/variants/accept` | POST | Mark as ACCEPTED | | `/variants/retry` | POST | Mark as REPLACED | | `/variants/followup` | POST | Mark as SKIPPED | | `/variants/feedback` | POST | Set LIKE/DISLIKE feedback | ## Notes - This database is **separate from the Dashboard database** (different Supabase instance) - All timestamps use `timestamptz` (timezone-aware) - UUIDs are generated server-side with `gen_random_uuid()` - The `desktop_projects` table in Dashboard DB corresponds to `project_id` here --- /docs/ai-infrastructure/keafab/ # KeaFab API Documentation KeaFab is Keak's AI-powered **Conversion Rate Optimization (CRO) variant generation API**. It takes UI components and generates optimized variants designed to improve conversion metrics. ## What is KeaFab? KeaFab is a specialized AI service that: - **Analyzes UI components** using vision models to understand visual hierarchy and content - **Routes to specialized optimizers** based on component type (hero, nav, pricing, etc.) - **Generates CRO-optimized variants** with copy, layout, and style improvements - **Validates output** to ensure code quality and constraint compliance Unlike generic LLM prompts, KeaFab uses domain-specific CRO expertise encoded into specialized agents, each trained on conversion optimization best practices for their component type. ## Why Specialized CRO AI? Generic AI models can rewrite code, but they lack: 1. **CRO Domain Knowledge** - Understanding of conversion psychology, awareness levels, friction points 2. **Component-Specific Strategies** - Hero sections need different optimization than pricing tables 3. **Constraint Awareness** - Preserving `data-keak-id` attributes, component references, and assets 4. **Selection Intent Matching** - Adjusting change scope based on what the user selected KeaFab solves these by routing to **6 specialized optimizers**, each with CRO expertise for their component type. ## High-Level Architecture ```mermaid flowchart LR subgraph Inputs SC[Screenshot] CODE[Component Code] PRMPT([User Prompt]) end ROUTER[Router] subgraph Optimizers HERO[Hero] NAV[Nav] PRICING[Pricing] SOCIAL[Social Proof] FORM[Form] FEAT[Feature Section] end VALIDATOR[Validator] OUT[Variant] SC --> ROUTER CODE --> ROUTER PRMPT -.-> ROUTER ROUTER -->|hero| HERO ROUTER -->|nav| NAV ROUTER -->|pricing| PRICING ROUTER -->|social_proof| SOCIAL ROUTER -->|form| FORM ROUTER -->|feature_section| FEAT HERO --> VALIDATOR NAV --> VALIDATOR PRICING --> VALIDATOR SOCIAL --> VALIDATOR FORM --> VALIDATOR FEAT --> VALIDATOR VALIDATOR --> OUT ``` **Three-Phase Pipeline:** | Phase | Agent | Model | Purpose | |-------|-------|-------|---------| | 1 | Router | GPT-4.1-nano | Classify component type (hero, nav, pricing, etc.) | | 2 | Optimizer | Gemini-3-flash | Generate CRO-optimized variant with vision analysis | | 3 | Validator | Claude-Haiku-4.5 | Check constraints and fix violations | ## Key Features ### Specialized Optimizers Six component-specific optimizers, each with CRO expertise: | Optimizer | Focus Areas | |-----------|-------------| | **Hero** | Awareness levels, copy audit, friction reduction, CTA optimization | | **Nav** | Link hierarchy, CTA prominence, label clarity | | **Pricing** | Plan anchoring, comparison clarity, value framing | | **Form** | Field reduction, label optimization, trust signals | | **Social Proof** | Attribution, specificity, credibility | | **Feature Section** | Feature→benefit reframing, scannability | ### Real-Time Streaming Server-Sent Events (SSE) provide progress updates during generation: ``` router → "Classifying component..." optimizer → "Optimizing hero..." validator → "Validating constraints..." complete → Final variant delivered ``` ### Constraint Validation KeaFab uses a two-stage validation pipeline: **1. Heuristic Detection** (`validation.py`) Fast regex-based checks that detect common issues: - HTML syntax in JSX (`class=` instead of `className`) - HTML event handlers (`onclick` instead of `onClick`) - HTML string styles (`style="..."` instead of `style={{}}`) - HTML comments (`` instead of `{/* */}`) - Missing or duplicated `data-keak-id` - Unbalanced tags **2. AI Validator** (only if issues detected) Full validation against 11 constraints: - `data-keak-id` preserved exactly once on root element - All `data-*` attributes maintained - Valid JSX syntax with balanced tags - No placeholder content - Original assets preserved - JSX syntax enforced (className, onClick, htmlFor) - JSX style objects (not string styles) - JSX comments (`{/* */}`) ## Documentation Structure | Page | Description | |------|-------------| | [Architecture](./architecture) | Deep dive into the 3-phase pipeline and design decisions | | [API Reference](./api-reference) | Endpoints, schemas, and usage examples | | [Optimizers](./optimizers) | How specialized optimizers work | | [DSPy Signatures](./dspy-signatures) | AI model integration and prompt engineering | | [Client Integration](./client-integration) | How Keak-Code integrates with KeaFab | ## Quick Example **Request:** ```json { "full_page": "
...full JSX source...
", "screenshot_url": "https://blob.keak.com/screenshot.png", "selected_component": "
...
", "user_prompt": "Increase demo signups" } ``` **Response:** ```json { "analysis": "CRO diagnosis with awareness level, friction points...", "changes": "- COPY: Headline reframed for problem-aware audience\n- STYLE: CTA contrast increased...", "variant": "
...optimized JSX...
", "usage": { "router": {...}, "optimizer": {...}, "validator": {...} } } ``` ## Next Steps - **Understand the architecture**: Read [Architecture](./architecture) for the full pipeline breakdown - **Integrate with KeaFab**: See [API Reference](./api-reference) for endpoint details - **Learn optimizer strategies**: Explore [Optimizers](./optimizers) for CRO techniques --- /docs/ai-infrastructure/keafab/api-reference # API Reference KeaFab exposes a REST API for CRO variant generation. All endpoints require authentication and return JSON responses. ## Base URL ``` Production: https://keak-code-production.up.railway.app Local: http://localhost:8000 ``` ## Authentication All endpoints require a Bearer token in the Authorization header: ```http Authorization: Bearer YOUR_API_KEY ``` API keys are managed through the Keak dashboard. Requests without valid authentication receive a `401 Unauthorized` response. ## Endpoints ### Health Check ```http GET / ``` Simple health check endpoint. **Response:** ```json { "status": "healthy" } ``` *** ### Generate Variant ```http POST /cro/generate ``` Generate a CRO-optimized variant synchronously. Returns the complete result when generation finishes. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `full_page` | string | Yes | Full page source code (JSX/HTML) | | `screenshot_url` | string | Yes | URL to page screenshot for visual context | | `selected_component` | string | Yes | Component source code to optimize | | `user_prompt` | string | No | Optimization goals (default: "Optimize for maximum conversions") | | `media` | string\[] | No | Reference image URLs (max 3 used) | | `tier` | string | No | Legacy field, ignored (backward compatibility) | #### Request Example ```json { "full_page": "export default function Page() {\n return (\n
\n
...
\n
...
\n
\n );\n}", "screenshot_url": "https://blob.keak.com/screenshots/abc123.png", "selected_component": "
\n

Welcome to Our Product

\n

The best solution for your needs

\n \n
", "user_prompt": "Increase demo signups, target enterprise buyers" } ``` #### Response Body | Field | Type | Description | |-------|------|-------------| | `analysis` | string | CRO diagnosis with insights, issues, and strategy | | `changes` | string | Bullet list of changes made (COPY/STYLE/STRUCTURE) | | `variant` | string | Optimized component code (drop-in replacement) | | `usage` | object | Token usage per agent (router, optimizer, validator) | #### Response Example ```json { "analysis": "**Selection Intent:** Broad hero optimization for enterprise conversion\n\n**Principle:** Problem-Aware Headline Formula\n\n**Why:** Enterprise buyers need to see ROI potential immediately\n\n**Metric:** Demo request rate\n\n**Current State:**\n- Generic headline lacks specificity\n- CTA \"Learn More\" is low-commitment, wrong for enterprise\n- No trust signals or social proof\n\n**Issues:**\n- h1 | Generic value prop | Doesn't address enterprise pain points\n- button | Weak CTA | \"Learn More\" won't drive demos\n\n**Opportunity:** Add enterprise trust signals (logos, security badges)", "changes": "- COPY: Headline reframed from generic to ROI-focused for enterprise\n- COPY: CTA changed from \"Learn More\" to \"Schedule Demo\"\n- STYLE: Added subtle gradient to headline for visual hierarchy\n- STRUCTURE: Added social proof micro-copy below CTA", "variant": "
\n

\n Cut Implementation Time by 60%\n

\n

Enterprise teams ship faster with our automated deployment platform

\n \n

Join 500+ enterprise teams

\n
", "usage": { "router": { "openai/gpt-4.1-nano": { "prompt_tokens": 145, "completion_tokens": 18, "total_tokens": 163 } }, "optimizer": { "gemini/gemini-3-flash": { "prompt_tokens": 2847, "completion_tokens": 692, "total_tokens": 3539 } } } } ``` #### Error Responses | Status | Description | |--------|-------------| | `400 Bad Request` | Invalid request body or missing required fields | | `401 Unauthorized` | Missing or invalid API key | | `500 Internal Server Error` | Generation failed (see error message) | *** ### Generate Variant (Streaming) ```http POST /cro/generate/stream ``` Generate a CRO-optimized variant with real-time progress updates via Server-Sent Events (SSE). #### Request Body Same as `/cro/generate`. #### Response Returns a `text/event-stream` with the following event types: ##### Progress Event Sent during each phase of generation. ``` data: {"type": "progress", "step": "router", "message": "Classifying component..."} data: {"type": "progress", "step": "optimizer", "message": "Optimizing hero..."} data: {"type": "progress", "step": "validator", "message": "Validating constraints..."} ``` | Field | Type | Description | |-------|------|-------------| | `type` | string | Always `"progress"` | | `step` | string | Current phase: `router`, `optimizer`, `validator`, `complete` | | `message` | string | Human-readable progress message | ##### Complete Event Sent when generation finishes successfully. ``` data: {"type": "complete", "data": {"analysis": "...", "changes": "...", "variant": "...", "usage": {...}}} ``` | Field | Type | Description | |-------|------|-------------| | `type` | string | Always `"complete"` | | `data` | object | Same structure as `/cro/generate` response | ##### Error Event Sent if generation fails. ``` data: {"type": "error", "message": "Error description"} ``` | Field | Type | Description | |-------|------|-------------| | `type` | string | Always `"error"` | | `message` | string | Error description | #### Client Example (JavaScript) ```javascript const response = await fetch('https://keafab.keak.com/cro/generate/stream', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ full_page: '...', screenshot_url: '...', selected_component: '...', user_prompt: 'Increase signups' }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const event = JSON.parse(line.slice(6)); if (event.type === 'progress') { console.log(`[${event.step}] ${event.message}`); } else if (event.type === 'complete') { console.log('Variant:', event.data.variant); } else if (event.type === 'error') { console.error('Error:', event.message); } } } ``` *** ### Get Prompt Suggestions ```http POST /cro/suggest ``` Generate goal-oriented prompt suggestions for a selected component. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `selected_component` | string | Yes | Component source code | | `source_code` | string | Yes | Full page source for context | #### Request Example ```json { "selected_component": "", "source_code": "export default function Page() { return
...
}" } ``` #### Response Body | Field | Type | Description | |-------|------|-------------| | `suggestions` | string\[] | Exactly 4 prompt suggestions (4-10 words each) | | `usage` | object | Token usage from suggester model | #### Response Example ```json { "suggestions": [ "Maximize free trial signups", "Make the CTA more urgent", "Target enterprise decision makers", "Keep it minimal and clean" ], "usage": { "suggester": { "openai/gpt-4o-mini": { "prompt_tokens": 234, "completion_tokens": 45, "total_tokens": 279 } } } } ``` #### Suggestion Characteristics - First suggestion is always a **conversion/action goal** - Suggestions vary across: conversion target, tone, constraints, emphasis - Reference specific elements when possible (e.g., "demo button", "Pro plan") - No second-person pronouns ("your") - uses "the" instead - No CRO jargon or generic optimization advice *** ### Repair Variant Code ```http POST /keafab/repair ``` Automatically fix syntax errors in variant code using AI with full file context. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `originalSourceFile` | string | No | Full source file before variant was applied | | `currentSourceFile` | string | Yes | Full source file with broken variant | | `variantCode` | string | Yes | The broken variant code | | `buildError` | string | Yes | Error message from build tool (SWC, Babel, etc.) | | `keakId` | string | Yes | Unique ID for variant element extraction | #### Request Example ```json { "originalSourceFile": "export default function Hero() {\n return (\n
\n

Original Headline

\n \n
\n );\n}", "currentSourceFile": "export default function Hero() {\n return (\n
Optimized Headline\n \n
\n );\n}", "variantCode": "
Optimized Headline\n \n
", "buildError": "Error: x Unexpected token `<`. Expected jsx identifier", "keakId": "hero-1" } ``` #### Response Body | Field | Type | Description | |-------|------|-------------| | `repairedCode` | string | Full repaired source file with syntax errors fixed | | `usage` | object | Token usage data | #### Response Example ```json { "repairedCode": "export default function Hero() {\n return (\n
\n

Optimized Headline

\n \n
\n );\n}", "usage": { "promptTokens": 1245, "completionTokens": 234, "totalTokens": 1479 } } ``` #### Repair Characteristics - **Context-aware**: Uses original and current files to understand intent - **Targeted**: Fixes only the syntax error, preserves semantic changes - **KeakId preservation**: Always maintains `data-keak-id` attribute - **Full file repair**: Returns complete file, not just variant - **Single attempt**: Designed for high accuracy, no retries #### Common Repairs - Unclosed JSX tags - Missing closing brackets - Incomplete attributes (e.g., `data-`) - Truncated code - Invalid JSX expressions - Nested tag mismatches *** ## Request/Response Schemas ### GenerationRequest ```typescript interface GenerationRequest { full_page: string; // Required: Full page JSX/HTML source screenshot_url: string; // Required: URL to page screenshot selected_component: string; // Required: Component to optimize user_prompt?: string; // Optional: Optimization goals media?: string[]; // Optional: Reference image URLs tier?: 'turbo' | 'pro' | 'max'; // Deprecated: Ignored } ``` ### GenerationResponse ```typescript interface GenerationResponse { analysis: string; // CRO diagnosis and strategy changes: string; // Bullet list of modifications variant: string; // Optimized component code usage?: { // Token usage per agent [agentName: string]: { [modelId: string]: { prompt_tokens: number; completion_tokens: number; total_tokens: number; } } }; } ``` ### SuggestRequest ```typescript interface SuggestRequest { selected_component: string; // Required: Component code source_code: string; // Required: Full page source } ``` ### SuggestResponse ```typescript interface SuggestResponse { suggestions: string[]; // Exactly 4 suggestions usage?: object; // Token usage } ``` ### RepairRequest ```typescript interface RepairRequest { originalSourceFile?: string; // Optional: Full file before variant currentSourceFile: string; // Required: Full file with broken variant variantCode: string; // Required: The broken variant code buildError: string; // Required: Error message from build tool keakId: string; // Required: Element ID for extraction } ``` ### RepairResponse ```typescript interface RepairResponse { repairedCode: string; // Full repaired source file usage?: { // Token usage promptTokens: number; completionTokens: number; totalTokens: number; }; } ``` ### StreamEvent ```typescript type StreamEvent = | { type: 'progress'; step: string; message: string } | { type: 'complete'; data: GenerationResponse } | { type: 'error'; message: string }; ``` *** ## Rate Limits Rate limits are enforced per API key: | Tier | Requests/minute | Concurrent requests | |------|-----------------|---------------------| | Free | 10 | 2 | | Pro | 60 | 10 | | Enterprise | 300 | 50 | Exceeded limits return `429 Too Many Requests`. *** ## Best Practices ### Screenshot Quality - Capture at **2x resolution** for better visual analysis - Include the **full viewport** - optimizer uses surrounding context - **Highlight the selected element** before capture (optional but helpful) ### Component Selection - Include the **root element** with `data-keak-id` - Preserve **all attributes** - optimizer needs them for context - Avoid selecting **partial components** - optimizer works best with complete elements ### User Prompts Good prompts are specific about goals: | Bad | Good | |-----|------| | "Make it better" | "Increase demo signups from enterprise visitors" | | "Optimize" | "Make the CTA more urgent, target busy executives" | | "Fix the hero" | "Reframe headline for problem-aware audience" | ### Handling Responses - **Always validate** the variant renders correctly before applying - **Check `changes`** to understand what was modified - **Review `analysis`** to learn the CRO reasoning - **Track `usage`** for cost monitoring --- /docs/ai-infrastructure/keafab/architecture # Architecture KeaFab uses a **three-phase pipeline** architecture where each phase is handled by a specialized AI agent optimized for its task. This design enables fast classification, high-quality generation, and reliable output. ## Pipeline Overview ```mermaid flowchart TB subgraph Inputs FP[full_page] SS[screenshot_url] SC[selected_component] UP[user_prompt] MD[media] end subgraph "Phase 1: Router" ROUTER[Router
GPT-4.1-nano] end subgraph "Phase 2: Optimizer" direction LR HERO[Hero] NAV[Nav] PRICING[Pricing] SOCIAL[Social Proof] FORM[Form] FEAT[Feature Section] end subgraph "Phase 3: Validator" VALIDATOR[Validator
Claude-Haiku-4.5] end subgraph Outputs ANALYSIS[analysis] CHANGES[changes] VARIANT[variant] USAGE[usage] end FP --> ROUTER SS --> ROUTER SC --> ROUTER UP --> ROUTER MD --> ROUTER ROUTER -->|hero| HERO ROUTER -->|nav| NAV ROUTER -->|pricing| PRICING ROUTER -->|social_proof| SOCIAL ROUTER -->|form| FORM ROUTER -->|feature_section| FEAT HERO --> VALIDATOR NAV --> VALIDATOR PRICING --> VALIDATOR SOCIAL --> VALIDATOR FORM --> VALIDATOR FEAT --> VALIDATOR VALIDATOR --> ANALYSIS VALIDATOR --> CHANGES VALIDATOR --> VARIANT VALIDATOR --> USAGE ``` All optimizers use **Gemini-3-flash** for vision-capable CRO analysis. ## Phase 1: Router The Router classifies the component type to route to the appropriate specialized optimizer. ### Purpose - Fast, lightweight classification - Enables specialized optimization strategies - Results are cached for identical inputs ### Model Choice: GPT-4.1-nano - **Why**: Extremely fast and inexpensive - **Caching**: Enabled - same component gets same classification - **Approach**: `dspy.Predict` (single-step, no chain-of-thought needed) ### Component Types | Type | Description | Routing | |------|-------------|---------| | `hero` | Above-fold, primary headline, main CTAs, value proposition | → HeroOptimizer | | `nav` | Navigation bar, header menu, logo + links | → NavOptimizer | | `pricing` | Plan comparison, pricing cards, feature checklists | → PricingOptimizer | | `form` | Input fields, contact forms, signup forms | → FormOptimizer | | `social_proof` | Testimonials, logo bars, reviews | → SocialProofOptimizer | | `feature_section` | Feature grids, benefit lists, how-it-works | → FeatureSectionOptimizer | | `footer` | Footer links, legal, secondary navigation | → NavOptimizer (fallback) | | `data_table` | Tabular data, comparison tables | → FeatureSectionOptimizer (fallback) | ## Phase 2: Optimizer The Optimizer is the core of KeaFab - a specialized agent that generates CRO-optimized variants. ### Purpose - Domain-specific CRO expertise for each component type - Vision-based analysis of screenshot context - Generate drop-in replacement code ### Model Choice: Gemini-3-flash - **Why**: Excellent vision capabilities, strong reasoning, good at code generation - **Vision**: Analyzes screenshot to understand visual hierarchy, contrast, spacing - **Caching**: Disabled - each optimization should consider full context ### Optimizer Inputs ```python source_code: str # Full page JSX/HTML for context screenshot_url: str # Visual reference (processed as dspy.Image) selected_component: str # The component to optimize user_prompt: str # User's goals (e.g., "Increase signups") media: List[str] # Optional reference images (max 3) ``` ### Optimizer Outputs ```python analysis: str # CRO diagnosis with insights and strategy variant: str # Optimized component code (drop-in replacement) impact: str # Expected metric impact changes_summary: str # Bullet list of changes (COPY/STYLE/STRUCTURE) ``` ### Optimization Strategy Each optimizer follows a **three-phase CRO strategy**: 1. **Diagnosis** - Analyze the component for CRO issues: - Awareness level mismatch - Copy audit (weak verbs, jargon, missing benefits) - Friction audit (form fields, unclear CTAs) - Trust gap (missing social proof, credentials) 2. **Strategy** - Plan optimizations based on diagnosis: - Headline reframing for target awareness level - CTA copy matching user's action - Trust signal placement 3. **Implementation** - Generate the variant: - Apply copy changes - Add visual enhancements (within constraints) - Preserve all required attributes and references ## Phase 3: Validator The Validator ensures output quality by checking constraints and fixing violations. ### Purpose - Catch code quality issues before delivery - Fix common violations automatically - Ensure `data-keak-id` integrity ### Model Choice: Claude-Haiku-4.5 - **Why**: Fast, precise, excellent at code analysis - **Caching**: Disabled - each validation is unique - **Approach**: `dspy.ChainOfThought` (reasoning helps find violations) ### Validation Constraints (10 Total) 1. **data-keak-id** - Appears exactly once, on root element only 2. **data-* attributes*\* - All original data attributes preserved 3. **Syntax validity** - All tags properly closed, valid JSX/HTML 4. **No new imports** - Component is self-contained 5. **No placeholders** - No TODO, FIXME, or lorem ipsum 6. **Assets preserved** - All images, expressions maintained 7. **No duplicate classes** - No conflicting class names 8. **Framework syntax** - className (React) vs class (HTML) matches original 9. **Comment syntax** - `{/* */}` (JSX) vs `` (HTML) matches original 10. **Expression syntax** - `{}` (JSX) vs native preserved ### Smart Validation Skipping To avoid unnecessary API calls, the Orchestrator performs a **heuristic check** before invoking the Validator: ```python def _needs_validation(original: str, variant: str) -> bool: # Skip validation if variant looks clean: # - No markdown code fences # - data-keak-id appears exactly once # - All data-* attributes present # - HTML is balanced (no missing closing tags) # - No module-level code (import/export/function) ``` If the heuristic passes, validation is skipped entirely, saving latency and cost. ## Design Decisions ### Why Three Phases? | Alternative | Problem | |-------------|---------| | Single model | No specialization, poor CRO quality | | Two phases (no validator) | Output quality issues, broken code | | More phases | Unnecessary latency, over-engineering | Three phases provide the right balance of **specialization**, **quality**, and **speed**. ### Why Specialized Optimizers? Generic prompts produce generic results. Each component type has unique CRO considerations: | Component | Unique CRO Factors | |-----------|-------------------| | Hero | Awareness levels, headline formula, above-fold psychology | | Pricing | Anchoring, decoy effect, plan comparison UX | | Form | Field psychology, label optimization, submit button copy | | Social Proof | Attribution trust, specificity, social proof types | Specialized optimizers encode this domain knowledge directly into their prompts. ### Why These Models? | Agent | Model | Rationale | |-------|-------|-----------| | Router | GPT-4.1-nano | Fastest classification, cacheable, low cost | | Optimizer | Gemini-3-flash | Best vision + code generation combo | | Validator | Claude-Haiku-4.5 | Fast validation, excellent at constraint checking | This combination optimizes for **speed** (fast models where possible) and **quality** (powerful models where needed). ## Progress Streaming KeaFab supports **Server-Sent Events (SSE)** for real-time progress updates. ### Implementation ```python # Progress callback invoked at each phase def progress_callback(step: str, message: str): # Emits SSE event to client pass # Called during orchestration: progress_callback('router', 'Classifying component...') progress_callback('optimizer', 'Optimizing hero...') progress_callback('validator', 'Validating constraints...') progress_callback('complete', 'Done') ``` ### Architecture ``` ┌──────────────┐ ┌─────────────────────┐ ┌──────────┐ │ Client │◀────│ SSE Event Stream │◀────│ Thread │ │ (Frontend) │ │ (text/event-stream)│ │ (Worker) │ └──────────────┘ └─────────────────────┘ └──────────┘ ▲ │ │ ▼ Progress Queue Orchestrator.generate() (Thread-safe) ``` - Generation runs in a **background thread** - Progress updates pushed to a **thread-safe queue** - Main async function streams events from the queue - Completion signaled via `threading.Event` ## Error Handling ### Generation Errors Errors during generation are: 1. Logged with structured JSON 2. Returned as SSE error event (streaming) or HTTP error (standard) 3. Include error type and message ### Validation Failures When validation finds violations: 1. Validator attempts to **fix** the variant 2. Returns fixed variant if successful 3. Returns original with violations list if unfixable ## Token Usage Tracking KeaFab tracks token usage across all three phases: ```python { "router": { "openai/gpt-4.1-nano": { "prompt_tokens": 150, "completion_tokens": 20 } }, "optimizer": { "gemini/gemini-3-flash": { "prompt_tokens": 3000, "completion_tokens": 800 } }, "validator": { "anthropic/claude-haiku-4-5": { "prompt_tokens": 500, "completion_tokens": 100 } } } ``` This enables cost tracking, usage analytics, and optimization of model choices. --- /docs/ai-infrastructure/keafab/client-integration # Client Integration This document describes how Keak-Code integrates with the KeaFab API for variant generation and prompt suggestions. ## Overview The integration consists of four main parts: 1. **`keafabGenerator.ts`** - Low-level service for variant generation API 2. **`keafabCodeRepair.ts`** - Low-level service for code repair API 3. **`use-keafab-generator.ts`** - React hook for variant generation 4. **`use-prompt-suggestions.ts`** - React hook for AI-powered prompt suggestions 5. **`use-code-repair.ts`** - React hook for error detection and auto-repair ``` ┌─────────────────────────────────────────────────────────────────┐ │ Keak-Code Frontend │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ ┌──────────────────────────┐ │ │ │ CRODrawer │────▶ │ useKeafabGenerator Hook │ │ │ │ (Component) │ │ - Screenshot capture │ │ │ │ │ │ - Progress tracking │ │ │ │ │ │ - Telemetry │ │ │ │ │ └──────────────────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌──────────────────────────┐ │ │ │ │ │ keafabGenerator Service │ │ │ │ │ │ - SSE stream parsing │ │ │ │ │ └──────────────────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ /keafab/generate (stream) │ │ │ │ │ │ │ │ ┌──────────────────────────┐ │ │ │ │────▶│ usePromptSuggestions Hook│ │ │ │ │ │ - Fetch suggestions │ │ │ │ │ │ - Cache by elementId │ │ │ │ │ └──────────────────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ /keafab/suggest │ │ │ │ │ │ │ │ ┌──────────────────────────┐ │ │ │ │────▶│ useCodeRepair Hook │ │ │ └─────────────────┘ │ - Error detection │ │ │ │ - Auto-repair trigger │ │ │ └──────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ keafabCodeRepair Service │ │ │ │ - Variant extraction │ │ │ └──────────────────────────┘ │ │ │ │ │ ▼ │ │ /keafab/repair │ │ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ Dashboard API Proxy │ │ /api/keafab/generate │ │ /api/keafab/suggest │ └────────────────────────────────┘ │ ▼ ┌──────────────────┐ │ KeaFab API │ │ /cro/generate │ │ /cro/suggest │ └──────────────────┘ ``` ## Request Flow All KeaFab requests flow through the Dashboard API proxy: ``` Keak-Code → Dashboard Proxy → KeaFab API ``` The dashboard proxy: - Authenticates requests using the user's session token - Forwards to KeaFab with the server-side API key - Handles CORS for cross-origin requests ## keafabGenerator Service **Location:** `src/renderer/src/services/ai/keafabGenerator.ts` The service provides a simple interface for calling the KeaFab API with streaming support. ### Types ```typescript interface KeafabGeneratorInput { fullPageSourceCode: string; // Full page JSX/TSX source selectedComponent: string; // Component to optimize userPrompt: string; // Optimization goals screenshotUrl: string; // Screenshot URL (blob storage) media?: string[]; // Optional reference images } interface KeafabGeneratorProgress { step: string; // 'router' | 'optimizer' | 'validator' | 'complete' message: string; // Human-readable progress message } interface KeafabGeneratorResult { variantCode: string; // Generated variant code analysis: string; // CRO analysis changes: string; // List of changes made usage?: KeafabUsageData; // Token usage data } ``` ### Main Function ```typescript async function generateVariantWithKeafab( input: KeafabGeneratorInput, onProgress?: (progress: KeafabGeneratorProgress) => void ): Promise ``` ### How It Works 1. **Makes POST request** to `/keafab/generate` (via dashboard proxy) 2. **Streams SSE events** from response body 3. **Parses events** and calls `onProgress` for progress updates 4. **Returns final result** when `complete` event received ### SSE Parsing ```typescript // Buffer for partial events let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // Split on double newline (SSE event separator) const lines = buffer.split('\n\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; const event = JSON.parse(line.slice(6)); if (event.type === 'progress') { onProgress?.({ step: event.step, message: event.message }); } else if (event.type === 'complete') { finalResult = event.data; } else if (event.type === 'error') { throw new Error(event.message); } } } ``` ## useKeafabGenerator Hook **Location:** `src/renderer/src/hooks/use-keafab-generator.ts` The hook provides a complete integration with state management, screenshot capture, and telemetry. ### Props ```typescript interface UseKeafabGeneratorProps { selectedElement: SelectedElement | null; // From preview iframe sourceCode: string; // Current file source originalSourceCode?: string | null; // Original (pre-edit) source chatId: string | null; // Chat session ID onProgress?: (progress: KeafabGeneratorProgress) => void; } ``` ### Return Value ```typescript interface UseKeafabGeneratorReturn { // State isGenerating: boolean; progress: KeafabGeneratorProgress | null; result: KeafabGeneratorResult | null; error: string | null; generationPhase: 'idle' | 'capturing' | 'uploading' | 'generating' | 'complete' | 'error'; // Actions generate: (userPrompt: string, options?: GenerateOptions) => Promise; reset: () => void; } ``` ### Generation Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ generate() called │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Phase: CAPTURING │ │ - Find preview iframe │ │ - Highlight selected element (blue box shadow) │ │ - Calculate iframe bounds │ │ - Capture screenshot via Electron API │ │ - Remove highlight │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Phase: UPLOADING │ │ - Convert screenshot to base64 │ │ - Upload to blob storage │ │ - Get unique file URL │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Phase: GENERATING │ │ - Extract component source code (not DOM HTML) │ │ - Call keafabGenerator with: │ │ - fullPageSourceCode (original source) │ │ - selectedComponent (extracted JSX) │ │ - userPrompt │ │ - screenshotUrl │ │ - media (optional) │ │ - Stream progress updates │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Phase: COMPLETE │ │ - Create CROVariant object │ │ - Track telemetry (tokens, duration) │ │ - Track PostHog event │ │ - Return result │ └─────────────────────────────────────────────────────────────────┘ ``` ### Screenshot Capture The hook captures screenshots of the preview iframe: ```typescript const captureAndUploadScreenshot = useCallback(async () => { // 1. Find preview iframe const previewIframe = document.querySelector( 'iframe[data-keak-preview="true"]' ); // 2. Highlight selected element const element = previewIframe.contentDocument.querySelector( `[data-keak-id="${keakId}"]` ); element.style.boxShadow = '0 0 0 4px rgba(59,130,246,0.8)'; await new Promise(resolve => setTimeout(resolve, 300)); // 3. Calculate bounds and capture const iframeBounds = getPreviewIframeBounds(); const dataUrl = await window.electronAPI.captureScreenshot(iframeBounds); // 4. Remove highlight element.style.boxShadow = ''; // 5. Upload to blob storage const result = await uploadScreenshot(dataUrl.split(',')[1]); return result.unique_file_url; }, []); ``` ### Source Code Extraction The hook extracts JSX source code (not DOM HTML) using source mapping: ```typescript const extractComponentSourceCode = useCallback(() => { const sourceToUse = originalSourceCode || sourceCode; const keakId = selectedElement.keakId; const sourceMapping = selectedElement.sourceMapping; // Find element boundaries in source code const boundaries = findElementBoundaries({ sourceLines: sourceToUse.split('\n'), keakId, startLine: sourceMapping.lineNumber - 1, startColumn: sourceMapping.columnNumber, }); if (boundaries?.foundClosing) { return sourceLines .slice(boundaries.startLine, boundaries.endLine + 1) .join('\n'); } // Fallback to DOM outerHTML return selectedElement.outerHTML; }, []); ``` ### Telemetry Integration The hook tracks generation metrics: ```typescript // Track generation start const { generationId } = await telemetry.trackGenerationStart({ chatId, sessionId, userPrompt, generationType, // INITIAL | FOLLOWUP | RETRY llmModel: 'keafab', screenshotUrl, mediaUrls: options?.media || [], hasScreenshot: true, }); // Track completion await telemetry.trackGenerationComplete({ generationId, durationMs, inputTokens, outputTokens, variant: variantData, llm_response: `${result.analysis}\n\n**Changes**\n${result.changes}` }); // Track in PostHog posthogService.track('variant_generated', { chat_id: chatId, generation_id: generationId, variant_id: variantId, duration_ms: durationMs, input_tokens: inputTokens, output_tokens: outputTokens, success: true, }); ``` ## Usage Example ```tsx function CRODrawer({ selectedElement, sourceCode, chatId }) { const { isGenerating, progress, generationPhase, generate, reset } = useKeafabGenerator({ selectedElement, sourceCode, chatId, onProgress: (p) => console.log(`[${p.step}] ${p.message}`) }); const handleGenerate = async () => { const result = await generate("Increase signups", { isRetry: false, promptConversationIndex: 0 }); if (result) { // Apply variant to preview applyVariant(result.variant); } }; return (
{isGenerating && (
{generationPhase} {progress?.message}
)}
); } ``` ## Error Handling The hook handles errors at each phase: | Phase | Error | Handling | |-------|-------|----------| | Capturing | Screenshot API unavailable | Returns null, continues without screenshot | | Uploading | Upload failed | Throws error, sets error state | | Generating | API error | Throws error, sets error state | | Generating | No result | Throws "No result received" | ```typescript try { const result = await generate(prompt); if (!result) { // Generation failed - check state.error } } catch (error) { // Unexpected error } ``` ## Token Usage Parsing The hook aggregates token usage across all agents: ```typescript function parseUsageTokens(usage) { let inputTokens = 0; let outputTokens = 0; // Sum across all agents (router, optimizer, validator) for (const agentName of Object.keys(usage)) { for (const modelId of Object.keys(usage[agentName])) { inputTokens += usage[agentName][modelId].prompt_tokens || 0; outputTokens += usage[agentName][modelId].completion_tokens || 0; } } return { inputTokens, outputTokens }; } ``` *** ## usePromptSuggestions Hook **Location:** `src/renderer/src/hooks/use-prompt-suggestions.ts` The `usePromptSuggestions` hook fetches AI-generated prompt suggestions from KeaFab's `/cro/suggest` endpoint. These suggestions help users understand what optimizations are possible for their selected component. ### Purpose When a user selects a component in the CRO drawer, this hook: 1. Sends the component HTML and page source to KeaFab 2. Receives 4 goal-oriented prompt suggestions 3. Displays them as clickable options in the UI ### Props ```typescript interface UsePromptSuggestionsOptions { enabled?: boolean; // Whether to fetch suggestions (default: true) elementId?: string; // Unique ID to trigger re-fetch on selection change selectedComponent?: string; // HTML of the selected component sourceCode?: string; // Full JSX source code of the page } ``` ### Return Value ```typescript interface UsePromptSuggestionsReturn { suggestions: string[]; // Array of 4 prompt suggestions isLoading: boolean; // True while fetching error: string | null; // Error message if fetch failed analysis: string | null; // Optional analysis from API } ``` ### How It Works ```typescript useEffect(() => { const fetchFromApi = async () => { const response = await fetch(`${backendUrl}/keafab/suggest`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ selected_component: selectedComponent, source_code: sourceCode || '' }) }); const data = await response.json(); if (data.success && data.data?.suggestions?.length > 0) { setSuggestions(data.data.suggestions); } }; fetchFromApi(); }, [enabled, elementId, selectedComponent, sourceCode]); ``` ### Re-fetch Behavior The hook re-fetches suggestions when any of these change: - `enabled` - Toggle suggestions on/off - `elementId` - Selected element changed - `selectedComponent` - Component HTML changed - `sourceCode` - Page source changed ### Usage Example ```tsx function CROPromptInput({ selectedElement, sourceCode }) { const { suggestions, isLoading, error } = usePromptSuggestions({ enabled: !!selectedElement, elementId: selectedElement?.keakId, selectedComponent: selectedElement?.outerHTML, sourceCode }); return (
{isLoading && } {suggestions.map((suggestion, index) => ( ))} {error &&

{error}

}
); } ``` ### Suggestion Characteristics The suggestions returned by KeaFab are: - **Goal-oriented** - Focus on conversion outcomes, not generic advice - **Specific** - Reference actual elements ("the demo button", "Pro plan") - **Varied** - Cover different optimization angles: - Conversion target ("Maximize demo signups") - Tone shift ("Make it more urgent") - Constraints ("Keep the headline") - Emphasis ("Focus on speed over features") - **Concise** - 4-10 words each ### Example Suggestions For a hero section with a "Get Started" button: ```json [ "Maximize free trial signups", "Make the CTA more urgent", "Target enterprise decision makers", "Keep it minimal and clean" ] ``` *** ## keafabCodeRepair Service **Location:** `src/renderer/src/services/ai/keafabCodeRepair.ts` The code repair service automatically fixes syntax errors in variant code using the Keafab repair API. ### Types ```typescript interface KeafabRepairRequest { currentSourceFile: string; // Full file with broken variant originalSourceFile?: string; // Full file before variant (optional) variantCode: string; // The broken variant code buildError: string; // Error message from build tool keakId: string; // Element ID for extraction } interface RepairResult { fixed: boolean; // Whether repair succeeded code: string; // Extracted variant code fullRepairedFile?: string; // Full repaired file for direct write error?: string; // Error message if failed usage?: { // Token usage data promptTokens: number; completionTokens: number; totalTokens: number; }; } ``` ### Main Function ```typescript async function repairCodeWithKeafab( originalSourceFile: string, currentSourceFile: string, variantCode: string, buildError: string, keakId: string ): Promise ``` ### How It Works 1. **Calls Keafab repair endpoint** via Dashboard proxy 2. **Sends full file context** (original + current + error) 3. **Receives repaired full file** from API 4. **Extracts variant by keakId** using JSX parsing 5. **Returns both** extracted variant and full file ### Variant Extraction ```typescript function extractVariantByKeakId(sourceCode: string, keakId: string): string | null ``` The extraction algorithm: 1. Searches for `data-keak-id` attribute (multiple formats) 2. Scans backward to find opening `<` tag 3. Identifies tag name 4. Handles self-closing tags (`/>`) 5. For regular tags, finds matching closing tag with depth tracking 6. Returns extracted element or null if not found ### Integration Patterns #### Preview-Time Repair Used in `use-variant-applier.ts` during preview: ```typescript // Attempt preview const result = await previewVariant(sourceMapping, variantCode, keakId); if ('error' in result) { // Preview failed - try repair const repairResult = await repairCodeWithKeafab( originalSourceRef.current || sourceCode, // originalSourceFile sourceCode, // currentSourceFile variantCode, // variantCode result.error, // buildError selectedElement?.keakId || '' // keakId ); if (repairResult.fixed) { // Retry preview with repaired code const retryResult = await previewVariant( sourceMapping, repairResult.code, // Use extracted variant keakId ); } } ``` #### Post-Build Repair Used in `use-code-repair.ts` when monitoring terminal output: ```typescript // Detect build error from terminal const errorInfo = parseBuildError(errorMessage); const recentVariant = findRecentVariantForFile(errorInfo.file, Date.now()); if (recentVariant) { // Read current file with broken variant const currentFile = await electronAPI.readFile(recentVariant.filePath); // Call repair API const repairResult = await repairCodeWithKeafab( recentVariant.originalCode, // originalSourceFile currentFile.content, // currentSourceFile recentVariant.variantCode, // variantCode errorMessage, // buildError recentVariant.keakId // keakId ); if (repairResult.fixed) { // Write full repaired file directly to disk await electronAPI.writeFile( recentVariant.filePath, repairResult.fullRepairedFile || repairResult.code ); // Notify CRO drawer to restore preview state window.postMessage({ type: 'KEAK_VARIANT_REPAIR_SUCCESS', keakId: recentVariant.keakId }, '*'); } else { // Repair failed - revert to original code await electronAPI.writeFile( recentVariant.filePath, recentVariant.originalCode ); // Notify CRO drawer that repair failed and was reverted window.postMessage({ type: 'KEAK_VARIANT_REPAIR_FAILED_REVERTED', keakId: recentVariant.keakId }, '*'); // Submit auto-report and show error modal await submitAutoReport({ ... }); } } ``` ### Error Handling ```typescript try { const repairResult = await repairCodeWithKeafab(...); if (repairResult.fixed) { // Success path } else { // Repair failed - submit auto-report await submitAutoReport({ trigger: 'Variant Generation Build Error (Auto-repair failed)', errorMessage, variantCode, keakId, variantId }, token, userEmail); } } catch (error) { // API error console.error('[Code Repair] Error calling repair API:', error); } ``` ### Dashboard Proxy Implementation **Location:** `dashboard-no-code-v3/app/api/keafab/repair/route.ts` ```typescript import { withAuth } from '@/app/api/lib/auth'; import { successResponse, errorResponse } from '@/app/api/lib/response'; const RepairRequestSchema = z.object({ currentSourceFile: z.string(), originalSourceFile: z.string().optional(), variantCode: z.string(), buildError: z.string(), keakId: z.string(), }); export const POST = withAuth(async (user, request) => { const origin = request.headers.get('origin'); const body = await request.json(); // Validate request const parsed = RepairRequestSchema.safeParse(body); if (!parsed.success) { return errorResponse( "Invalid request body: " + parsed.error.message, 400, ErrorCodes.BAD_REQUEST.code, origin ); } try { // Forward to KeaFab const response = await fetch(`${KEAFAB_API_URL}/keafab/repair`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${KEAFAB_API_KEY}`, }, body: JSON.stringify(parsed.data), }); if (!response.ok) { const errorText = await response.text(); return errorResponse( `KeaFab repair failed: ${errorText}`, response.status, ErrorCodes.BAD_GATEWAY.code, origin ); } const data = await response.json(); return successResponse(data, 200, origin); } catch (error) { console.error('[Keafab Repair] Error:', error); return handleApiError(error, origin); } }); // Handle OPTIONS preflight export async function OPTIONS(request: Request) { return handlePreflight(request.headers.get('origin')); } ``` *** ## Dashboard API Proxy All hooks communicate with KeaFab through the Dashboard API proxy, which handles authentication and forwards requests. ### Proxy Routes **Location:** `dashboard-no-code-v3/app/api/keafab/` | Route | KeaFab Endpoint | Purpose | |-------|-----------------|---------| | `/api/keafab/generate` | `/cro/generate/stream` | Variant generation (SSE) | | `/api/keafab/suggest` | `/cro/suggest` | Prompt suggestions | | `/api/keafab/repair` | `/keafab/repair` | Code repair | ### Suggest Route **Location:** `app/api/keafab/suggest/route.ts` ```typescript // Request validation const SuggestRequestSchema = z.object({ selected_component: z.string(), source_code: z.string(), }); // Handler export const POST = withAuth(async (user, request) => { const body = await request.json(); const parsed = SuggestRequestSchema.safeParse(body); if (!parsed.success) { return errorResponse("Invalid request body", 400); } // Forward to KeaFab const upstreamResponse = await fetch(`${KEAFAB_API_URL}/cro/suggest`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${KEAFAB_API_KEY}` }, body: JSON.stringify(parsed.data) }); const data = await upstreamResponse.json(); return successResponse(data); }); ``` ### Authentication Flow ``` ┌─────────────┐ User Token ┌─────────────┐ API Key ┌─────────────┐ │ Keak-Code │ ───────────────▶ │ Dashboard │ ───────────▶│ KeaFab │ │ (Frontend) │ │ (Proxy) │ │ (API) │ └─────────────┘ └─────────────┘ └─────────────┘ ``` 1. **Keak-Code** sends user's Bearer token to Dashboard 2. **Dashboard** validates token via `withAuth` middleware 3. **Dashboard** forwards request to KeaFab with server-side API key 4. **KeaFab** processes request and returns response 5. **Dashboard** forwards response to Keak-Code --- /docs/ai-infrastructure/keafab/dspy-signatures # DSPy Signatures KeaFab uses [DSPy](https://github.com/stanfordnlp/dspy) for structured AI model interactions. DSPy provides type-safe signatures that define inputs, outputs, and system prompts for each agent. ## What is DSPy? DSPy is a framework for programming language models with: - **Signatures** - Type-safe input/output contracts - **Modules** - Composable LM programs (Predict, ChainOfThought, etc.) - **Optimizers** - Automatic prompt optimization - **Caching** - Built-in response caching KeaFab uses DSPy 3.1.0 for all AI agent interactions. ## Why DSPy? | Benefit | How KeaFab Uses It | |---------|-------------------| | **Type Safety** | Inputs/outputs are validated Python types | | **Structured Prompts** | System prompts defined in signature docstrings | | **Composability** | Agents can be chained and nested | | **Usage Tracking** | Built-in token counting per call | | **Caching** | Router results cached for identical inputs | ## Signature Overview | Signature | Agent | Purpose | |-----------|-------|---------| | `RouterSignature` | Router | Classify component type | | `ValidatorSignature` | Validator | Check constraints, fix violations | | `SuggesterSignature` | Suggester | Generate prompt suggestions | | `HeroSignature` | HeroOptimizer | Optimize hero sections | | `NavSignature` | NavOptimizer | Optimize navigation | | `PricingSignature` | PricingOptimizer | Optimize pricing sections | | `FormSignature` | FormOptimizer | Optimize forms | | `SocialProofSignature` | SocialProofOptimizer | Optimize testimonials | | `FeatureSectionSignature` | FeatureSectionOptimizer | Optimize feature displays | ## Router Signature Classifies components into one of 8 types for routing. ```python class RouterSignature(dspy.Signature): """Classify UI component type for CRO optimization routing.""" component: str = dspy.InputField( desc="Component source code (JSX)" ) page_context: str = dspy.InputField( desc="Brief description of page context and user intent" ) element_type: Literal[ "hero", "nav", "pricing", "social_proof", "form", "feature_section", "footer", "data_table" ] = dspy.OutputField( desc="Component classification" ) rationale: str = dspy.OutputField( desc="One sentence explaining classification" ) ``` **Usage:** ```python router = dspy.Predict(RouterSignature) result = router( component="
...
", page_context="Landing page for SaaS product" ) # result.element_type = "hero" # result.rationale = "Contains primary headline and CTA above the fold" ``` ## Pre-Validation Heuristics Before invoking the AI validator, `validation.py` performs fast regex checks: ```python from app.agents.validation import needs_validation # Returns True if any issues detected if needs_validation(original, variant): # Invoke validator to fix issues result = validator.validate(original, variant) ``` **Checks performed:** - `has_html_class_attribute()` - Detects `class=` instead of `className` - `has_html_event_handlers()` - Detects `onclick` instead of `onClick` - `has_html_for_attribute()` - Detects `for=` instead of `htmlFor` - `has_html_comments()` - Detects `` instead of `{/* */}` - `has_html_style_attribute()` - Detects `style="..."` instead of `style={{}}` - `has_invalid_keak_id()` - Checks data-keak-id placement - `has_unbalanced_tags()` - Checks tag balance - `has_module_level_code()` - Detects imports/exports in fragments ## Validator Signature Validates generated variants and fixes JSX syntax violations. ```python class ValidatorSignature(dspy.Signature): """Validate and fix CRO variant JSX code. JSX syntax only, never HTML.""" original: str = dspy.InputField( desc="Original component source code" ) variant: str = dspy.InputField( desc="Generated variant to validate" ) violations: str = dspy.OutputField( desc="[PASS/FAIL] for each constraint checked" ) fixed_variant: str = dspy.OutputField( desc="Corrected variant with violations fixed" ) ``` **Constraints Checked:** 1. `data-keak-id` exactly once on root element 2. All `data-*` attributes preserved 3. Valid JSX syntax, balanced tags 4. No new imports required 5. No placeholder content 6. All assets preserved 7. No duplicate/conflicting classes 8. JSX syntax only: `className`, `onClick`, `htmlFor` 9. JSX style objects: `style={{}}` not `style="..."` 10. JSX comments: `{/* */}` not `` 11. No module-level code in fragments ## Suggester Signature Generates 4 goal-oriented prompt suggestions for users. ```python class SuggesterSignature(dspy.Signature): """Generate CRO-focused prompt suggestions for component optimization.""" selected_component: str = dspy.InputField( desc="Component code to optimize" ) source_code: str = dspy.InputField( desc="Full page source for context" ) suggestions: List[str] = dspy.OutputField( desc="Exactly 4 goal-oriented prompts (4-10 words each)" ) ``` **Suggestion Guidelines:** - First suggestion must be conversion/action goal - Vary across: conversion target, tone, constraints, emphasis - Reference specific elements when possible - No second-person ("your") - use "the" instead - No CRO jargon or generic advice ## Optimizer Signatures All optimizers share a common pattern with component-specific system prompts. ### Base Pattern ```python class BaseOptimizerSignature(dspy.Signature): """[Component-specific CRO expertise and instructions]""" # Inputs source_code: str = dspy.InputField(desc="Full page source") screenshot: dspy.Image = dspy.InputField(desc="Page screenshot") selected_component: str = dspy.InputField(desc="Component to optimize") user_prompt: str = dspy.InputField(desc="User's optimization goals") media: Optional[List[dspy.Image]] = dspy.InputField(desc="Reference images") # Outputs analysis: str = dspy.OutputField(desc="CRO diagnosis") variant: str = dspy.OutputField(desc="Optimized component code") impact: str = dspy.OutputField(desc="Expected metric impact") changes_summary: str = dspy.OutputField(desc="Bullet list of changes") ``` ### Hero Signature ```python class HeroSignature(dspy.Signature): """ You are a CRO expert specializing in hero section optimization. THREE-PHASE CRO STRATEGY: 1. CRO DIAGNOSIS - Awareness Level: Unaware → Problem-Aware → Solution-Aware → Product-Aware → Most-Aware - Copy Audit: Weak verbs, jargon, missing benefits, generic claims - Friction Audit: Form fields, unclear CTAs, too many options - Trust Gap: Missing social proof, credentials, specificity 2. COPY STRATEGY - Headline: Reframe for target awareness level - Subhead: Support headline, add proof or context - CTA: Match user's desired action 3. VISUAL ENHANCEMENT (within constraints) - Gradient text for key words - Glow shadows for CTAs - Avatar stacks for social proof - Micro-copy for friction reduction CONSTRAINTS: - Preserve data-keak-id exactly - Preserve all data-* attributes - Never wrap root in new container - Output valid JSX syntax only """ source_code: str = dspy.InputField() screenshot: dspy.Image = dspy.InputField() selected_component: str = dspy.InputField() user_prompt: str = dspy.InputField() media: Optional[List[dspy.Image]] = dspy.InputField() analysis: str = dspy.OutputField() variant: str = dspy.OutputField() impact: str = dspy.OutputField() changes_summary: str = dspy.OutputField() ``` ### Other Optimizer Signatures Each optimizer follows the same pattern with specialized system prompts: | Signature | System Prompt Focus | |-----------|---------------------| | `NavSignature` | Link hierarchy, CTA prominence, label clarity | | `PricingSignature` | Anchoring, plan comparison, friction-appropriate CTAs | | `FormSignature` | Field reduction, label optimization, trust signals | | `SocialProofSignature` | Attribution, specificity, credibility signals | | `FeatureSectionSignature` | Feature→benefit, scannability, visual hierarchy | ## Shared Prompt Templates Optimizers use shared templates from `desc.py`. Prompts are kept minimal—detailed syntax rules live in the validator. ### Variant Output Rules ```python VARIANT_PREFIX = """ The variant MUST be valid JSX that is a drop-in replacement for the selected_component only. We ONLY generate JSX syntax (for .jsx/.tsx files), NEVER HTML. """ SELECTION_INTENT = """ Match your changes to selection granularity: - Small selection (button, heading) → focused changes - Large selection (full section) → broader optimization """ VARIANT_OUTPUT_RULES = """ CRITICAL - OUTPUT ONLY THE JSX COMPONENT FRAGMENT: - No markdown code fences - No imports/exports - No function declarations - Just the JSX fragment """ VARIANT_PRESERVE_RULES = """ PRESERVE: - All data-* attributes exactly - All references - All image sources and {expressions} JSX syntax: className (not class), onClick (not onclick), style={{}} (not style="") """ ``` The brief JSX reminder at the end of `VARIANT_PRESERVE_RULES` leverages LLM recency bias while keeping the optimizer focused on CRO. ### Analysis Format ```python ANALYSIS_FORMAT = """ **Selection Intent:** [What user likely wants] **Principle:** [CRO principle name] **Why:** [Why it fits this component] **Metric:** [Primary metric to improve] **Current State:** - [Observation] **Issues:** - [element] | [problem] | [why it hurts conversion] **Context:** [How component fits page flow] **Opportunity:** [Optional higher-impact opportunity] """ ``` ### Changes Format ```python CHANGES_FORMAT = """ - COPY: [text change] → [benefit] - STYLE: [visual change] → [benefit] - STRUCTURE: [layout change] → [benefit] Maximum 6 changes per variant. """ ``` ## DSPy Modules Used | Module | Purpose | Used By | |--------|---------|---------| | `dspy.Predict` | Single-step generation | Router, Suggester | | `dspy.ChainOfThought` | Reasoning before output | All Optimizers, Validator | | `dspy.Image` | Image input handling | Optimizers (screenshot, media) | | `dspy.LM` | Language model interface | All agents | ## Model Configuration ```python # Router - fast classification router_lm = dspy.LM("openai/gpt-4.1-nano", cache=True) # Optimizer - vision + reasoning optimizer_lm = dspy.LM("gemini/gemini-3-flash") # Validator - fast validation validator_lm = dspy.LM("anthropic/claude-haiku-4-5", cache=False) # Suggester - prompt generation suggester_lm = dspy.LM("openai/gpt-4o-mini") ``` ## Usage Tracking DSPy tracks token usage automatically: ```python with dspy.context(lm=self.lm, track_usage=True): result = self.optimizer( source_code=source, screenshot=dspy.Image(screenshot_url), selected_component=component, user_prompt=prompt ) usage = result.get_lm_usage() ``` Returns: ```python { "gemini/gemini-3-flash": { "prompt_tokens": 2847, "completion_tokens": 692, "total_tokens": 3539 } } ``` --- /docs/ai-infrastructure/keafab/endpoints # KeaFab Endpoints Complete reference for all KeaFab API endpoints including variant generation, prompt suggestions, and code repair. ## Base URL ``` Production: https://keak-code-production.up.railway.app Local: http://localhost:8000 ``` ## Authentication All endpoints require a Bearer token in the Authorization header: ```http Authorization: Bearer YOUR_API_KEY ``` API keys are managed through the Keak dashboard. Requests without valid authentication receive a `401 Unauthorized` response. *** ## Variant Generation Endpoints ### Generate Variant ```http POST /cro/generate ``` Generate a CRO-optimized variant synchronously. See [API Reference](./api-reference.md#generate-variant) for full details. ### Generate Variant (Streaming) ```http POST /cro/generate/stream ``` Generate a CRO-optimized variant with real-time progress updates via Server-Sent Events (SSE). See [API Reference](./api-reference.md#generate-variant-streaming) for full details. *** ## Prompt Suggestions Endpoint ### Get Prompt Suggestions ```http POST /cro/suggest ``` Generate goal-oriented prompt suggestions for a selected component. See [API Reference](./api-reference.md#get-prompt-suggestions) for full details. *** ## Code Repair Endpoint ### Repair Variant Code ```http POST /keafab/repair ``` Automatically fix syntax errors in variant code using AI with full file context. #### Purpose The repair endpoint is designed to fix build errors caused by malformed variant code. It receives: - Original source file (before variant was applied) - Current source file (with broken variant) - Variant code (the broken code) - Build error message - KeakId (for extracting the repaired variant) The AI analyzes the error in context and returns a fully repaired file. #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `originalSourceFile` | string | No | Full source file before variant (optional, provides context) | | `currentSourceFile` | string | Yes | Full source file with broken variant | | `variantCode` | string | Yes | The broken variant code | | `buildError` | string | Yes | Error message from build tool | | `keakId` | string | Yes | Unique ID for variant element extraction | #### Request Example ```json { "originalSourceFile": "export default function Hero() {\n return (\n
\n

Original Headline

\n \n
\n );\n}", "currentSourceFile": "export default function Hero() {\n return (\n
Optimized Headline\n \n
\n );\n}", "variantCode": "
Optimized Headline\n \n
", "buildError": "Error: x Unexpected token `<`. Expected jsx identifier\n ╭─[hero.tsx:3:1]\n 3 │
Optimized Headline", "keakId": "hero-1" } ``` #### Response Body | Field | Type | Description | |-------|------|-------------| | `repairedCode` | string | Full repaired source file | | `usage` | object | Token usage data | #### Response Example ```json { "repairedCode": "export default function Hero() {\n return (\n
\n

Optimized Headline

\n \n
\n );\n}", "usage": { "promptTokens": 1245, "completionTokens": 234, "totalTokens": 1479 } } ``` #### Error Responses | Status | Description | |--------|-------------| | `400 Bad Request` | Invalid request body or missing required fields | | `401 Unauthorized` | Missing or invalid API key | | `500 Internal Server Error` | Repair failed (see error message) | #### Repair Strategy The repair endpoint uses a multi-phase approach: 1. **Context Analysis** - Analyzes original vs current file to understand intent - Identifies what changed between versions - Understands the build error in full context 2. **Error Localization** - Pinpoints exact location of syntax error - Identifies affected JSX structure - Determines scope of required fix 3. **Targeted Repair** - Fixes syntax error while preserving intent - Maintains all attributes (including `data-keak-id`) - Ensures proper JSX structure 4. **Validation** - Verifies repaired code is syntactically valid - Ensures keakId is preserved - Confirms element structure is correct #### Common Error Types Fixed - **Unclosed tags**: `` - **Missing closing brackets**: `` - **Invalid JSX**: `{value > 0 &&` → `{value > 0 &&
Valid JSX
}` - **Malformed attributes**: `data-` → (removed incomplete attribute) - **Truncated code**: Incomplete elements restored - **Nested tag mismatches**: Balanced opening/closing tags #### Client Integration **From Keak-Code IDE**: ```typescript import { repairCodeWithKeafab } from './services/ai/keafabCodeRepair'; const repairResult = await repairCodeWithKeafab( originalSourceFile, currentSourceFile, variantCode, buildError, keakId ); if (repairResult.fixed) { // Write fullRepairedFile to disk await writeFile(filePath, repairResult.fullRepairedFile); // Or use extracted variant code console.log('Repaired variant:', repairResult.code); } ``` **Dashboard API Proxy**: ```typescript // dashboard-no-code-v3/app/api/keafab/repair/route.ts export const POST = withAuth(async (user, request) => { const body = await request.json(); // Validate request const parsed = RepairRequestSchema.safeParse(body); if (!parsed.success) { return errorResponse("Invalid request body", 400); } // Forward to KeaFab const response = await fetch(`${KEAFAB_API_URL}/keafab/repair`, { method: 'POST', headers: { 'Authorization': `Bearer ${KEAFAB_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(parsed.data) }); const data = await response.json(); return successResponse(data); }); ``` #### Best Practices **Include Full Context**: - Always send `originalSourceFile` when available - Provides AI with intent context - Improves repair quality **Send Complete Error Messages**: - Include line/column numbers - Include error code if available - Include file path in error message **Preserve KeakIds**: - Always include `keakId` in request - Ensures correct variant can be extracted - Critical for FileStateManager integration **Handle Extraction Failures**: - If `extractVariantByKeakId` fails, use `fullRepairedFile` - Write entire file to disk instead - FileStateManager will handle on next read #### Rate Limits Same as other endpoints: | Tier | Requests/minute | Concurrent requests | |------|-----------------|---------------------| | Free | 10 | 2 | | Pro | 60 | 10 | | Enterprise | 300 | 50 | #### Telemetry Repair calls are tracked separately from generation: ```json { "event": "repair_attempt", "keakId": "hero-1", "success": true, "usage": { "promptTokens": 1245, "completionTokens": 234 }, "errorType": "syntax", "durationMs": 1234 } ``` *** ## Related Documentation - [API Reference](./api-reference.md) - Complete API schemas and examples - [Client Integration](./client-integration.md) - IDE integration patterns - [Architecture](./architecture.md) - System design and agent flow --- /docs/ai-infrastructure/keafab/integration # KeaFab Integration Documentation coming soon. > **TODO**: Add integration guide for KeaFab API. --- /docs/ai-infrastructure/keafab/optimizers/ # Optimizers KeaFab uses **specialized optimizers** for different component types. Each optimizer encodes domain-specific CRO expertise, ensuring high-quality variants tailored to the component's purpose. ## How Routing Works When a request arrives, the **Router** classifies the component into one of 8 types: ``` Component → Router → element_type → Specialized Optimizer ``` ### Component Types | Type | Description | Optimizer | |------|-------------|-----------| | `hero` | Above-fold sections, primary headline, main CTAs | [HeroOptimizer](./hero) | | `nav` | Navigation bars, header menus, logo + links | [NavOptimizer](./nav) | | `pricing` | Pricing cards, plan comparisons, feature lists | [PricingOptimizer](./pricing) | | `form` | Input fields, contact forms, signup forms | [FormOptimizer](./form) | | `social_proof` | Testimonials, logo bars, reviews, case studies | [SocialProofOptimizer](./social-proof) | | `feature_section` | Feature grids, benefit lists, how-it-works | [FeatureSectionOptimizer](./feature-section) | | `footer` | Footer links, legal, secondary navigation | NavOptimizer (fallback) | | `data_table` | Tabular data, comparison tables | FeatureSectionOptimizer (fallback) | ### Fallback Mappings Two component types don't have dedicated optimizers: - **`footer`** → Uses `NavOptimizer` (similar link/CTA structure) - **`data_table`** → Uses `FeatureSectionOptimizer` (basic enhancement) ## Shared Constraints All optimizers enforce these constraints on generated variants: ### Required Preservations | Constraint | Description | |------------|-------------| | `data-keak-id` | Must appear exactly once, on the root element only | | `data-*` attributes | All original data attributes preserved | | Component references | `` (PascalCase) preserved | | Images & expressions | All `src`, `{expressions}` maintained | | Framework syntax | `className` (React) vs `class` (HTML) matches original | | Comment syntax | `{/* */}` (JSX) vs `` (HTML) matches original | ### Prohibited Actions | Constraint | Description | |------------|-------------| | No wrapping | Root element cannot be wrapped in new container (unless adding siblings) | | No new imports | Component must be self-contained | | No placeholders | No TODO, FIXME, lorem ipsum, or "\[placeholder]" | | No module-level code | No `import`, `export`, `function` declarations | ## Output Format All optimizers return the same structure: ### Analysis Structured CRO diagnosis following this format: ``` **Selection Intent:** [What user likely wants to optimize] **Principle:** [CRO principle being applied] **Why:** [Why this principle fits this component] **Metric:** [Primary metric this optimization targets] **Current State:** - [Observation about current component] **Issues:** - [element] | [problem] | [why it hurts conversion] **Context:** [How component fits in page flow] **Opportunity:** [Optional higher-impact opportunity] ``` ### Changes Summary Bullet list of modifications, categorized by type: ``` - COPY: [Text change] → [Benefit] - STYLE: [Visual change] → [Benefit] - STRUCTURE: [Layout change] → [Benefit] ``` Maximum 6 changes per variant. ### Impact Expected metric impact in format: ``` [Metric] | [How changes drive action] (≤15 words) ``` Example: `Demo requests | ROI-focused headline + urgent CTA reduces friction for enterprise buyers` ### Variant The optimized component code as a **drop-in replacement**: - Same root element tag - `data-keak-id` on root element - All original attributes preserved - Ready to render without modifications ## Selection Intent Matching Optimizers adjust their **scope of changes** based on what the user selected: | Selection | Behavior | |-----------|----------| | Single element (button, heading) | Focused changes to that element only | | Small section (CTA group) | Changes within the section | | Large section (full hero) | Broader changes, can restructure | | Entire component | Full optimization with layout changes | This ensures the optimizer respects user intent - if they selected just a button, don't rewrite the entire hero. ## Model Configuration All optimizers use the same base model: | Setting | Value | |---------|-------| | Model | `gemini/gemini-3-flash` | | Type | Vision-capable LLM | | Approach | `dspy.ChainOfThought` | | Caching | Disabled | The model receives: - Full page source (for context) - Screenshot (for visual analysis) - Selected component (the optimization target) - User prompt (optimization goals) - Media URLs (optional reference images) ## Optimizer Pages Each optimizer has domain-specific CRO strategies: - **[Hero Optimizer](./hero)** - Awareness levels, copy formulas, CTA psychology - **[Nav Optimizer](./nav)** - Link hierarchy, CTA prominence, mobile UX - **[Pricing Optimizer](./pricing)** - Anchoring, decoy effect, value framing - **[Form Optimizer](./form)** - Field reduction, label optimization, trust signals - **[Social Proof Optimizer](./social-proof)** - Attribution, specificity, credibility - **[Feature Section Optimizer](./feature-section)** - Feature→benefit, scannability --- /docs/ai-infrastructure/keafab/optimizers/feature-section # Feature Section Optimizer The Feature Section Optimizer specializes in **feature displays** - feature grids, benefit lists, how-it-works sections, and capability showcases. It focuses on transforming features into benefits and improving scannability. ## When Triggered Router classifies a component as `feature_section` when it contains: - Feature grids or cards - Benefit lists with icons - How-it-works steps - Capability comparisons - Product highlights Also used as **fallback** for `data_table` components. ## CRO Strategy ### Key Focus Areas | Area | What It Optimizes | |------|-------------------| | **Feature→Benefit Reframing** | Technical features become user outcomes | | **Scannability** | Visual hierarchy for quick comprehension | | **Visual Hierarchy** | Important features get visual priority | | **Layout Optimization** | Grid structure serves the content | ### Feature Psychology The optimizer applies these principles: | Principle | Application | |-----------|-------------| | **Benefits > Features** | "Saves 10 hours/week" beats "Automated workflows" | | **Concrete > Abstract** | Numbers and specifics beat vague claims | | **Scanning Patterns** | F-pattern and Z-pattern reading behavior | | **Chunking** | Group related features for comprehension | ## Key Optimizations ### Feature→Benefit Reframing Before: Feature-focused ```html

Automated Workflows

Create custom automation rules for your processes.

``` After: Benefit-focused ```html

Save 10+ Hours Every Week

Automate repetitive tasks so your team focuses on what matters.

``` ### Scannability Improvement Before: Wall of text ```html

Our platform includes real-time analytics, team collaboration tools, custom integrations, and 24/7 support to help you succeed.

``` After: Scannable list ```html
📊 Real-time analytics
👥 Team collaboration
🔌 Custom integrations
💬 24/7 support
``` ### Visual Hierarchy Before: All features equal ```html
Feature A
Feature B
Feature C
``` After: Key features emphasized ```html
Feature B
Feature C
``` ### Layout Optimization Before: Single column, hard to scan ```html
  • Feature 1
  • Feature 2
  • Feature 3
  • Feature 4
  • Feature 5
  • Feature 6
``` After: Grid layout, easy to scan ```html
Feature 1
Feature 2
Feature 3
Feature 4
Feature 5
Feature 6
``` ## Constraints Feature section-specific constraints: | Constraint | Reason | |------------|--------| | Preserve all features | Don't remove feature items | | Keep icons/images | Don't remove visual elements | | Maintain grouping | Don't reorganize feature categories | | Preserve links | Don't remove "Learn more" links | ## Example ### Input ```jsx

Features

Cloud Storage

Store your files in the cloud with 99.9% uptime.

Real-time Sync

Changes sync instantly across all devices.

Team Sharing

Share files and folders with your team easily.

``` **User Prompt:** "Make features more compelling" ### Output **Analysis:** ``` **Selection Intent:** Feature section optimization for engagement **Principle:** Feature→Benefit Transformation **Why:** Features describe product, benefits describe user outcomes **Metric:** Feature section engagement / scroll depth **Current State:** - Headlines are feature names, not benefits - Descriptions are technical, not outcome-focused - No visual differentiation between features **Issues:** - "Cloud Storage" | Technical name | Doesn't convey benefit - "99.9% uptime" | Spec, not benefit | Should be "never lose work" - All features | Equal weight | No hierarchy for scanning **Opportunity:** Lead with user outcomes, add visual icons ``` **Changes:** ``` - COPY: Headlines reframed as benefits - COPY: Descriptions focus on user outcomes - STYLE: Added subtle visual hierarchy with icons - STRUCTURE: Grid layout for better scannability ``` **Variant:** ```jsx

Why Teams Choose Us

☁️

Never Lose Your Work

Your files are automatically backed up with 99.9% uptime guarantee.

⚡

Work From Anywhere

Changes sync instantly - pick up exactly where you left off on any device.

👥

Collaborate Effortlessly

Share files and folders with your team in one click.

``` --- /docs/ai-infrastructure/keafab/optimizers/form # Form Optimizer The Form Optimizer specializes in **form components** - signup forms, contact forms, lead capture, and input fields. It focuses on reducing friction and increasing completion rates. ## When Triggered Router classifies a component as `form` when it contains: - Input fields (text, email, password, etc.) - Form labels and placeholders - Submit buttons - Validation messages or helper text ## CRO Strategy ### Key Focus Areas | Area | What It Optimizes | |------|-------------------| | **Field Reduction** | Fewer fields = higher completion | | **Label Optimization** | Clear, scannable, action-oriented labels | | **Submit Button Copy** | Benefit-driven, specific to the action | | **Trust Signals** | Privacy notices, security badges | ### Form Psychology The optimizer applies these principles: | Principle | Application | |-----------|-------------| | **Cognitive Load** | Fewer fields reduce mental effort | | **Progressive Disclosure** | Show only essential fields first | | **Input Affordance** | Field styling signals expected input | | **Commitment Consistency** | Once started, users tend to finish | ## Key Optimizations ### Field Count Reduction Before: Too many required fields ```html
``` After: Essential fields only ```html

We'll ask for details later

``` ### Label Clarity Before: Ambiguous labels ```html ``` After: Specific, helpful labels ```html ``` ### Submit Button Copy Before: Generic submit ```html ``` After: Benefit-driven CTA ```html ``` ### Trust Signals Before: No trust indicators ```html
``` After: Trust signals added ```html

🔒 No spam. Unsubscribe anytime.

``` ## Constraints Form-specific constraints: | Constraint | Reason | |------------|--------| | Preserve all fields | Don't remove input fields | | Keep field types | Don't change input types (email, password, etc.) | | Maintain validation | Don't remove required attributes | | Preserve form action | Don't modify form submission | ## Example ### Input ```jsx

Contact Us

``` **User Prompt:** "Increase form completions" ### Output **Analysis:** ``` **Selection Intent:** Form optimization for completion rate **Principle:** Friction Reduction **Why:** Every additional field reduces completion by ~10% **Metric:** Form completion rate **Current State:** - 5 input fields is above optimal (3-4) - Phone and Company appear optional but create friction - Generic "Send" button lacks motivation **Issues:** - Phone field | Optional but visible | Creates perceived burden - Company field | Low-value data | Asks before value delivered - button | Generic copy | "Send" doesn't promise outcome **Opportunity:** Progressive disclosure - collect extra info after initial contact ``` **Changes:** ``` - STRUCTURE: Phone and Company marked clearly as optional - COPY: Submit button changed to outcome-focused - COPY: Added helper text to reduce anxiety - STYLE: Optional fields visually de-emphasized ``` **Variant:** ```jsx

Contact Us

We respond within 24 hours

``` --- /docs/ai-infrastructure/keafab/optimizers/hero # Hero Optimizer The Hero Optimizer specializes in **above-fold sections** - the first thing visitors see. It focuses on headline psychology, awareness-level matching, and CTA optimization. ## When Triggered Router classifies a component as `hero` when it contains: - Primary headline (h1) with value proposition - Above-fold positioning - Main call-to-action buttons - Supporting subheadline or description ## CRO Strategy ### Three-Phase Approach #### Phase 1: CRO Diagnosis The optimizer analyzes the hero for conversion issues: | Analysis Area | What It Checks | |---------------|----------------| | **Awareness Level** | Is the copy targeting the right audience awareness? | | **Copy Audit** | Weak verbs, jargon, missing benefits, generic claims | | **Friction Audit** | Unclear CTA, too many options, missing context | | **Trust Gap** | Missing social proof, credentials, or specificity | #### Phase 2: Copy Strategy Based on diagnosis, the optimizer plans changes: | Strategy | When Used | |----------|-----------| | **Headline Reframe** | Generic headline → specific benefit or outcome | | **Subhead Support** | Headline carries load → subhead adds proof/context | | **CTA Matching** | CTA doesn't match desired action → action-specific copy | #### Phase 3: Visual Enhancement Within constraints, the optimizer may add: | Enhancement | Purpose | |-------------|---------| | Gradient text | Draw attention to key headline words | | Glow shadows | Make CTA stand out | | Avatar stacks | Social proof at a glance | | Micro-copy | Reduce friction ("No credit card required") | ## Awareness Levels The optimizer matches copy to visitor awareness level: | Level | Visitor State | Copy Approach | |-------|---------------|---------------| | **Unaware** | Doesn't know they have a problem | Lead with the problem | | **Problem-Aware** | Knows the problem, not the solution | Agitate problem, introduce solution | | **Solution-Aware** | Knows solutions exist, not yours | Differentiate your solution | | **Product-Aware** | Knows your product, needs convincing | Focus on benefits, proof, urgency | | **Most-Aware** | Ready to buy | Minimize friction, clear CTA | The optimizer infers awareness from: - Page type (landing page vs. pricing page) - Copy specificity - CTA strength - User prompt hints ## Key Optimizations ### Headline Formula Before: Generic, feature-focused ```html

The Best Project Management Tool

``` After: Outcome-focused, specific ```html

Ship Projects 2x Faster with Zero Missed Deadlines

``` ### CTA Copy Before: Low-commitment, vague ```html ``` After: Action-specific, benefit-driven ```html No credit card required ``` ### Social Proof Integration Before: No proof elements ```html

...

``` After: Proof added without overwhelming ```html

...

Trusted by 10,000+ teams

``` ## Constraints Hero-specific constraints the optimizer follows: | Constraint | Reason | |------------|--------| | Keep hero structure | Don't convert to different section type | | Preserve CTA count | Don't add/remove CTAs without prompting | | Maintain brand voice | Match existing tone unless asked to change | | Above-fold focus | Changes should enhance, not bloat | ## Example ### Input ```jsx

Welcome to Our Platform

We help businesses grow with our innovative solutions.

``` **User Prompt:** "Increase demo bookings from SaaS founders" ### Output **Analysis:** ``` **Selection Intent:** Full hero optimization for SaaS founder conversion **Principle:** Problem-Aware Headline Formula **Why:** SaaS founders are problem-aware - they know growth is hard, need to see specific outcome **Metric:** Demo booking rate **Current State:** - Generic "Welcome" headline provides no value - "Innovative solutions" is meaningless jargon - "Get Started" CTA is vague **Issues:** - h1 | No outcome or benefit | Founders scroll past generic headlines - p | Buzzword-heavy | "Innovative solutions" means nothing - button | Unclear action | "Get Started" doesn't promise demo **Opportunity:** Add founder-specific social proof (ARR milestones, logos) ``` **Changes:** ``` - COPY: Headline reframed to outcome "Scale to $1M ARR" - COPY: Subhead changed from jargon to specific benefit - COPY: CTA changed to "Book Demo" with supporting micro-copy - STYLE: Added gradient to headline for visual impact ``` **Variant:** ```jsx

Scale to $1M ARR Without Burning Out

The growth platform that helped 500+ SaaS founders hit their revenue goals

Free 30-min strategy call

``` --- /docs/ai-infrastructure/keafab/optimizers/nav # Nav Optimizer The Nav Optimizer specializes in **navigation components** - headers, menus, and nav bars. It focuses on link hierarchy, CTA prominence, and label clarity. ## When Triggered Router classifies a component as `nav` when it contains: - Navigation links (Products, Pricing, About, etc.) - Logo or brand element - Header/menu structure - Navigation CTAs (Sign In, Get Started) Also used as **fallback** for `footer` components. ## CRO Strategy ### Key Focus Areas | Area | What It Optimizes | |------|-------------------| | **Link Hierarchy** | Most important links get visual priority | | **CTA Prominence** | Primary CTA stands out from navigation links | | **Label Clarity** | Links use clear, action-oriented language | | **Mobile Considerations** | Hamburger menu, touch targets, spacing | ### Analysis Approach The optimizer evaluates: 1. **Link Priority** - Are high-value pages (Pricing, Demo) easily accessible? 2. **CTA Visibility** - Does the primary CTA stand out? 3. **Label Scannability** - Can users find what they need quickly? 4. **Visual Balance** - Is the nav cluttered or sparse? ## Key Optimizations ### CTA Differentiation Before: CTA blends with nav links ```html ``` After: CTA visually distinct ```html ``` ### Label Clarity Before: Ambiguous labels ```html Solutions Resources ``` After: Specific, scannable labels ```html Use Cases Docs & Guides ``` ### Link Hierarchy Before: Flat structure, no priority ```html ``` After: High-value links prioritized ```html ``` ## Constraints Nav-specific constraints: | Constraint | Reason | |------------|--------| | Preserve all links | Don't remove navigation options | | Keep structure | Don't convert nav to different layout | | Maintain accessibility | Links must remain keyboard-navigable | | Logo preservation | Never modify or remove logo | ## Example ### Input ```jsx ``` **User Prompt:** "Increase trial signups" ### Output **Analysis:** ``` **Selection Intent:** Nav optimization for trial conversion **Principle:** CTA Prominence Hierarchy **Why:** Sign Up CTA competes visually with Login and nav links **Metric:** Trial signup click-through rate **Current State:** - Sign Up has no visual distinction from Login - Both CTAs same style as nav links **Issues:** - signup link | No visual weight | Blends with secondary links - login link | Equal prominence | Competes with primary CTA **Context:** Nav is primary conversion path for visitors exploring site ``` **Changes:** ``` - STYLE: Sign Up button styled as primary CTA - STYLE: Login styled as secondary/ghost button - COPY: "Sign Up" → "Start Free Trial" (specificity) ``` **Variant:** ```jsx ``` --- /docs/ai-infrastructure/keafab/optimizers/pricing # Pricing Optimizer The Pricing Optimizer specializes in **pricing sections** - plan comparisons, pricing cards, and feature checklists. It focuses on psychological pricing principles like anchoring and the decoy effect. ## When Triggered Router classifies a component as `pricing` when it contains: - Price values or pricing cards - Plan comparisons (Free, Pro, Enterprise) - Feature checklists per plan - Plan selection CTAs ## CRO Strategy ### Key Focus Areas | Area | What It Optimizes | |------|-------------------| | **Anchoring** | Display order influences perception of value | | **Plan Comparison** | Make differences clear and meaningful | | **Friction Reduction** | CTAs appropriate to plan commitment level | | **Value Framing** | Price presentation affects perceived value | ### Pricing Psychology The optimizer applies these principles: | Principle | Application | |-----------|-------------| | **Anchoring** | Show highest price first to make others seem affordable | | **Decoy Effect** | Middle option made attractive by strategic feature placement | | **Loss Aversion** | Highlight what users miss by not upgrading | | **Social Proof** | "Most Popular" badges reduce decision paralysis | ## Key Optimizations ### Plan Ordering Before: Low to high (wrong anchor) ```html
Free - $0
Pro - $29
Enterprise - $99
``` After: High to low (proper anchoring) ```html
Enterprise - $99
Free - $0
``` ### Feature Differentiation Before: Features don't differentiate plans ```html
  • ✓ Feature A
  • ✓ Feature B
  • ✓ Feature C
``` After: Clear value progression ```html
  • ✓ 5 projects
  • ✓ Basic analytics
  • ✗ Priority support
  • ✗ Custom integrations
``` ### CTA Matching Before: Same CTA for all plans ```html ``` After: Friction-appropriate CTAs ```html ``` ### Value Framing Before: Raw price display ```html

$29/month

``` After: Value-framed pricing ```html

$29 /month

Save 20% annually

``` ## Constraints Pricing-specific constraints: | Constraint | Reason | |------------|--------| | Preserve all prices | Don't modify actual price values | | Keep all plans | Don't remove pricing options | | Maintain feature lists | Don't add/remove features | | Preserve billing info | Don't change billing terms | ## Example ### Input ```jsx

Pricing

Starter

$0/month

  • 3 projects
  • Basic features

Pro

$49/month

  • Unlimited projects
  • Advanced features
  • Priority support
``` **User Prompt:** "Increase Pro plan conversions" ### Output **Analysis:** ``` **Selection Intent:** Pricing section optimization for Pro conversions **Principle:** Anchoring + Social Proof **Why:** Pro plan needs visual distinction and validation **Metric:** Pro plan signup rate **Current State:** - Both plans have identical visual treatment - No indication which plan is recommended - Same CTA copy doesn't differentiate commitment **Issues:** - Pro plan | No visual priority | Blends with Starter - button | Same copy | Doesn't indicate value difference **Opportunity:** Add "Most Popular" badge and feature comparison ``` **Changes:** ``` - STYLE: Pro plan highlighted with border and badge - COPY: Added "Most Popular" social proof badge - COPY: CTAs differentiated by plan type - STRUCTURE: Pro plan features expanded for comparison ``` **Variant:** ```jsx

Pricing

Starter

$0/month

  • 3 projects
  • Basic features
Most Popular

Pro

$49/month

  • Unlimited projects
  • Advanced features
  • Priority support
``` --- /docs/ai-infrastructure/keafab/optimizers/social-proof # Social Proof Optimizer The Social Proof Optimizer specializes in **trust and credibility elements** - testimonials, customer logos, reviews, and case study snippets. It focuses on attribution, specificity, and credibility signals. ## When Triggered Router classifies a component as `social_proof` when it contains: - Customer testimonials or quotes - Logo bars (customer/partner logos) - Review scores or star ratings - Case study cards or metrics - Trust badges or certifications ## CRO Strategy ### Key Focus Areas | Area | What It Optimizes | |------|-------------------| | **Attribution Completeness** | Name, role, company, photo | | **Specificity** | Vague praise → concrete results | | **Credibility Signals** | Verifiable details that build trust | | **Format Assessment** | Right type of proof for the context | ### Social Proof Psychology The optimizer applies these principles: | Principle | Application | |-----------|-------------| | **Authority** | Titles, companies, credentials matter | | **Similarity** | Prospects trust people like themselves | | **Specificity** | Numbers and details beat generalities | | **Recency** | Recent testimonials feel more relevant | ## Key Optimizations ### Attribution Enhancement Before: Anonymous testimonial ```html
"Great product, highly recommend!" - Happy Customer
``` After: Full attribution ```html
"Great product, highly recommend!" Sarah Chen Sarah Chen VP of Engineering, Acme Corp
``` ### Specificity Improvement Before: Vague praise ```html

"This tool saved us so much time!"

``` After: Concrete results ```html

"This tool cut our deployment time from 4 hours to 15 minutes"

``` ### Credibility Signals Before: Just a logo ```html Company ``` After: Logo with context ```html
Company Since 2019
``` ### Format Optimization Before: Text-heavy testimonial ```html

"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation..."

``` After: Scannable highlight + source ```html

"Cut our costs by 40%"

"The ROI was clear within the first month..."

CFO, Fortune 500 Company
``` ## Constraints Social proof-specific constraints: | Constraint | Reason | |------------|--------| | Preserve quotes | Don't modify testimonial text | | Keep attributions | Don't remove or change names/companies | | Maintain images | Don't alter logos or photos | | Preserve metrics | Don't change numbers or statistics | ## Example ### Input ```jsx

What Our Customers Say

"Really good software, we use it every day."

- John

"Helped our team a lot."

- Marketing Team
``` **User Prompt:** "Make testimonials more credible" ### Output **Analysis:** ``` **Selection Intent:** Testimonials optimization for credibility **Principle:** Attribution Authority **Why:** Anonymous or partial attribution reduces trust significantly **Metric:** Trust perception / time-on-section **Current State:** - First testimonial has first name only, no role/company - Second testimonial is team-attributed, not individual - Both quotes are vague, no specific outcomes **Issues:** - "John" | No context | Could be anyone, no authority - "Marketing Team" | No individual | Team quotes feel manufactured - Both quotes | Vague praise | "Good" and "helped" prove nothing **Opportunity:** Add photos, full names, roles, and pull specific metrics from quotes ``` **Changes:** ``` - STRUCTURE: Added avatar placeholders for future images - COPY: Attribution enhanced with role and company - STYLE: Testimonial layout improved for scannability - COPY: Added suggestion prompts for specific metrics ``` **Variant:** ```jsx

What Our Customers Say

"Really good software, we use it every day."

John Martinez · Product Manager

"Helped our team a lot."

Lisa Chen · Marketing Director

``` --- /docs/ai-infrastructure/sdk/javascript # Keak-SDK documentation --- /docs/ai-infrastructure/sdk/python # Python SDK Documentation coming soon. > **TODO**: Add Python SDK documentation and examples. --- /docs/ai-infrastructure/sdk/rest-api # REST API Documentation coming soon. > **TODO**: Add REST API documentation. --- /docs/billing/changelog # Billing changelog Dated log of billing changes. Add new entries at the top when shipping. One line per change is enough. > **Audience:** Keak engineering and ops. Searchable history. ## 2026-05-31: Billing sync initiative ### Critical drift fixes - Scheduled missing crons in `vercel.json`: `meter-sync` (hourly), `payment-failures` (every 15 min), `process-downgrades` (daily 09:30 UTC), `credit-expiry` (daily 00:15 UTC) - Expanded legacy Stripe price-ID map (14 prices); unmapped prices default to `FREE` not `PRO` - Admin plan endpoint accepts `starter` ### POC payment links - `PocPaymentLink` table + CRM create flow - `checkout.session.completed` → `handlePocPayment` when `payment_link` set - Redemption creates `BillingCredit` + `billing_mode='metered'`; Slack thread on redeem ### Credit balance model - `BillingCredit` + `BillingCreditTransaction` tables; FIFO consumption in `credit-service.ts` - Meter-sync drains credits before Stripe reporting; entitlements gate respects pre-card credit balance ### Reconciliation - `POST /api/admin/billing/reconcile`, dry-run + apply mode; CRM dialog at Billing → Reconcile with Stripe ### Auto-pause on card removal - `customer.subscription.deleted` / `payment_method.detached` auto-pause all user tests - `POST /api/billing/resume-auto-paused` after re-adding card; `PaymentMethodBanner` in dashboard ### Multi-currency plumbing - `BillingSubscription.currency`, `IMPRESSION_PRICE_BY_CURRENCY`, `UnpricedCurrencyError` ### CRM & email drift - Subscriptions table: legacy price display, POC flags, credit columns, currency - POC Links tab; reconcile surfaces `EMAIL_DRIFT` / `EMAIL_CONFLICT` ### Documentation - `app/docs/billing/` subtree (this section) ## How to add an entry When shipping billing changes, add a dated section at the top. Reference the PR or commit. Keep entries terse. --- /docs/billing/credits # Billing credits A **billing credit** is prepaid balance on your Keak account. Keak applies credits **before** charging your card for impressions. ## How credits work 1. You receive a credit grant, for example from a POC deposit arranged with the Keak team 2. Impressions consume credit at **$0.005 each** ($0.50 per 100 impressions) 3. While balance remains, your card is not charged for covered impressions 4. When balance hits zero, normal pay-as-you-go billing resumes, if a payment method is on file **Example:** A $300 credit ≈ **60,000 impressions**. After that, impressions either bill to your card or stop if you have no payment method on file. ## Expiry Credits may have an **expiry date**. Unused balance is forfeited after expiry. Keak uses credits closest to expiry first. ## No payment method yet If you have **no card** but positive credit, you can launch tests and serve impressions until the balance runs out. After that, add a payment method on [Billing](https://platform.keak.com/dashboard/billing) to keep tests live. ## Viewing balance See **remaining credit**, **total granted**, and **expiry** (if any) on [Dashboard → Billing](https://platform.keak.com/dashboard/billing). --- /docs/billing/free-tier-gate # Free-tier impression gate Accounts without a payment method on file can still use Keak to **build and preview** tests. They **cannot serve impressions** to real visitors until they add a card or have a positive **credit balance**. > **Audience:** Keak ops and support. See [Billing overview](/docs/billing/overview) for the customer-facing summary. ## What the gate protects Keak blocks **live traffic** (impressions and downstream conversions) for accounts that haven't set up billing yet. Product usage inside the extension and dashboard still works, the gate only stops real visitors from being bucketed into unpaid tests. ## What's allowed vs blocked | Action | No card, no credit | No card, has credit | Card on file | | --- | --- | --- | --- | | Generate variants | Monthly quota limits | Unlimited | Unlimited | | Preview in extension | Yes | Yes | Yes | | Launch a live test | Blocked | Yes | Yes | | Serve impressions to visitors | Blocked | Yes until credit runs out | Yes until cap | | Record conversions | No (no impressions) | Yes | Yes | ## When the gate applies **At launch:** Trying to turn a test live without billing coverage shows a payment-required message in the dashboard. The test stays off until the account has a card or credit. **At serve time:** If a test somehow goes live without coverage, impressions are **not counted** and visitors are **not bucketed** into variants. The site still loads normally; Keak skips the test for that visitor. > **Note:** Impressions fail silently at serve time (no error shown to the visitor). The dashboard shows a **billing banner** on affected tests with a link to [Billing](https://platform.keak.com/dashboard/billing). This avoids noisy retries from the extension's background analytics. ## Workspace coverage A team member without their own card may still serve impressions if they work on a **domain owned by someone who has billing set up**. Coverage follows the **domain owner**, not every individual seat. Implementation: `lib/billing/workspace-coverage.ts` ## How customers fix it 1. Add a payment method on [Dashboard → Billing](https://platform.keak.com/dashboard/billing), or 2. Redeem a POC / prepaid **credit:** see [Credits](/docs/billing/credits) After billing is in place, launch or resume tests from the dashboard. Paused tests can be resumed once coverage is restored. ## Technical reference ### Where the gate fires **Launch:** `canLaunchExperiment(userId)` in the toggle-status API. Pre-card users without coverage get `402` with `PAYMENT_METHOD_REQUIRED`. **Serve:** `checkImpressionGate(domain.user_id)` in the analytics record API. Returns **200** to the client (avoids extension retry loops) but **does not count** the impression. ### UI signal `PaymentMethodBanner` (`components/billing/PaymentMethodBanner.tsx`) appears on test pages when `canReceiveImpressions.allowed === false`. It links to [Billing](https://platform.keak.com/dashboard/billing). ### Health check `GET /api/admin/billing/gate-health` (admin-only) runs synthetic checks: pre-card blocked, complimentary allowed, cap-exceeded blocked. Wire into uptime monitoring, regressions either grant free impressions or block paying customers. ### Why 200 on serve, not 402? The extension batches analytics in the background. A 402 would cause retry noise. Silent skip + dashboard banner is the intended UX. --- /docs/billing/overview # Billing overview How Keak charges for serving tests to real visitors. Manage your account on [Dashboard → Billing](https://platform.keak.com/dashboard/billing). ## How billing works Keak uses **pay-as-you-go impressions** for most accounts: - **$0.005 per impression** when a visitor is bucketed into a live test - **AI variants, images, and videos:** not billed per generation on metered plans - Invoices aggregate usage through Stripe at the end of each billing period You need either a **payment method on file** or a positive **credit balance** before impressions reach real visitors. ## Account states **No payment method:** You can build and preview tests in the extension, but live tests won't receive impressions until you add a card or have account credit from a Keak grant. The dashboard shows a banner when impressions are blocked. **Metered (card on file):** Impressions are counted and billed at $0.005 each. Usage caps and auto top-up may apply to your account. **Legacy fixed plans:** Some accounts remain on older monthly or annual subscriptions. Impressions are allowed under those plan terms. **Complimentary access:** Keak may grant metered access with no charges (may include an impression cap). **Credit balance:** Prepaid balance applies before your card is charged. See [Credits](/docs/billing/credits). ## What happens without a payment method - **Launch** may be blocked with a payment-required message - **Impressions** are not counted until billing is set up (visitors still see your site) - **Conversions** won't accumulate if impressions aren't served Add a card on [Billing](https://platform.keak.com/dashboard/billing) or apply account credit to go live. ## Workspace coverage Team members on a domain owned by a paying user may inherit impression coverage from the **domain owner:** not every seat on the team needs its own payment method. ## When your subscription ends If your subscription is cancelled or your payment method is removed, Keak may pause active tests until billing is restored. After re-adding a card, you can resume paused tests from the dashboard. --- /docs/billing/poc-payment-links # POC payment links Branded Stripe checkout links for proof-of-concept deals. Keak ops create a link in the CRM; the customer pays; the account gets **metered access** and a **credit balance** automatically, no manual billing setup on your side. > **Audience:** Keak ops and sales. Not a customer self-serve flow. ## When to use a POC link Use a POC payment link when a prospect or customer needs **time-boxed access** to run live tests before (or instead of) adding their own card: - One-off POC payment (most common) - Optional recurring monthly pricing for longer trials - Configurable grant length, impression cap, and assigned customer email The customer completes Stripe checkout; Keak handles the rest. ## Create a link 1. Open **CRM → Billing → POC Links** 2. Click **Create POC link** and fill in: - **Product name:** e.g. "Keak Pro for Acme" (shown on checkout) - **Description:** optional subtitle on the Stripe page - **Pricing:** one-off (default) or recurring monthly - **Amount:** USD - **Grant duration:** how many days of metered access (default 30) - **Impression cap:** leave blank for unlimited during the grant - **Assigned customer email:** optional; only that email can redeem the link 3. Submit. Keak creates the Stripe checkout link and posts a summary to the sales Slack channel 4. Share the checkout URL with the customer Checkout branding (logo, colors) is configured in **Stripe Dashboard → Settings → Branding**. API: `POST /api/admin/billing/poc-payment-links` creates Stripe Product + Price + Payment Link in one call. ## What happens when the customer pays After successful checkout, Keak automatically: 1. Matches the payment to the POC link you created 2. Finds the Keak account for the payer's email (must match an existing user) 3. Grants **credit** equal to the amount paid, with an expiry based on grant duration 4. Enables **metered billing** on the account so impressions can run 5. Marks the link as redeemed and replies in the Slack thread from link creation The customer can launch tests and serve impressions using that credit. See [Credits](/docs/billing/credits) for how balance is consumed. > **Tip:** If the customer has no Keak account yet, they need to sign up with the same email they use at checkout (or the assigned email, if you set one). ## Technical reference ### Redemption flow Stripe fires `checkout.session.completed`. The billing webhook routes to `handlePocPayment` in `lib/billing/poc-service.ts`, which: 1. Looks up `PocPaymentLink` by `session.payment_link` 2. Verifies `reserved_for_email` matches the payer (if set) 3. Finds the Keak user by email 4. Creates a `BillingCredit` equal to amount paid, with `expires_at = now + grant_days` 5. Sets `billing_mode='metered'` on the user's subscription 6. Stamps redemption on the link and replies in Slack on the create thread ### Assigned email mismatch If `reserved_for_email` is set and a different email pays, Stripe still charges but **no grant is issued:** logged as `[poc] reservation mismatch`. Ops decides on manual refund. ### Archive Archiving deactivates the Stripe Payment Link and sets `archived_at` locally. Old URLs show a Stripe error page. Rows remain for audit. ## Slack notifications Each link generates two Slack messages (payment processing is never blocked if Slack fails): - **Create:** amount, grant length, cap, assigned customer, links to checkout and Stripe - **Redeem:** threaded reply with payer, credit expiry, links to the user and link in CRM Use the redeem thread to confirm the customer is live without opening the CRM. --- /docs/billing/reconciliation # Reconciliation Walk every Stripe subscription and local `BillingSubscription` row and produce a **drift report** with one verdict per finding. > **Audience:** Keak ops. CRM only. Never writes to Stripe; apply mode mutates the Keak DB only. ## Run a dry-run 1. CRM → **Billing** → **Reconcile with Stripe** 2. Click **Run dry-run reconcile** Takes 10–30 seconds (live Stripe pagination). No writes. ## Verdicts | Verdict | Meaning | Auto-apply? | | --- | --- | --- | | **OK** | DB and Stripe agree | — | | **MISSING\_IN\_DB** | Stripe has sub; we don't | Yes (if user resolvable) | | **MISSING\_IN\_STRIPE** | We have sub; Stripe doesn't | Yes → pre\_card, canceled | | **DUPLICATE\_STRIPE** | Customer has ≥2 active Stripe subs | **No:** cancel older sub manually | | **PLAN\_MISMATCH** | DB plan/mode differs from Stripe price map | Yes | | **POC\_PROTECTED** | Active comp grant with POC expiry stamp | Never | | **ORPHAN\_STRIPE** | Active Stripe sub, no Keak user | **No:** manual link | | **EMAIL\_DRIFT** | Stripe email ≠ Keak user email | Yes (updates user + Supabase auth) | | **EMAIL\_CONFLICT** | Stripe email maps to different Keak user | **No:** manual merge | ## Apply mode Review the dry-run, select verdict types to commit, click **Apply N selected**. Each fix runs in its own try/catch, partial failure surfaces per row. `DUPLICATE_STRIPE` and `ORPHAN_STRIPE` are excluded from apply UI by design. ## When to run - Before a billing cycle close - After bulk Stripe dashboard changes - After a webhook outage - Monthly cadence for most accounts ## Implementation - Route: `app/api/admin/billing/reconcile/route.ts` - Price map: `lib/billing/legacy-price-map.ts` - POC expiry: `parsePocExpiryFromNote` in `lib/billing/poc-service.ts` **Safety:** Idempotent re-runs; POC\_PROTECTED and EMAIL\_CONFLICT never auto-apply. --- /docs/billing/usage-based # Usage-based billing Metered billing charges **$0.005 per impression** delivered to a real visitor. AI variants, images, and videos are unlimited and not billed separately. ## What counts as an impression An impression is recorded when a visitor lands on a page where an **active** test runs and Keak assigns them to a variant. If your account can't serve impressions (no payment method, no credit, or usage cap reached), the visitor still sees the site, but Keak does not count the impression. Check usage on [Dashboard → Billing](https://platform.keak.com/dashboard/billing) or **Usage** in the sidebar. ## How charges accumulate 1. A visitor views a page with an active test and is assigned a variant 2. Keak checks that your account can serve impressions (payment method, credit balance, or plan limits) 3. If allowed, the impression is added to your current billing period 4. Any [credits](/docs/billing/credits) on your account are applied first 5. Remaining usage is billed through Stripe at period end. **$0.005 × impressions** ## Usage caps and auto top-up Some accounts have a **monthly usage cap**. When you reach it, new impressions pause until the next billing period or until your cap is adjusted. If **auto top-up** is enabled on your account, Keak charges your card when spend crosses a preset threshold so tests keep running. You'll be notified if a top-up fails. Contact if you need cap or top-up changes. ## What you see on invoices Your Stripe invoice shows impression quantity at **$0.005** each. Credit-covered impressions reduce what is charged to your card. You can preview usage on the billing page before the invoice closes. ## Without a payment method Accounts without a payment method don't accumulate billable impressions. See [Billing overview](/docs/billing/overview) for what is blocked and how to add a card. --- /docs/extension-ide/ # Extension & IDE The user-facing tools that make A/B testing accessible to everyone. Whether you're working in a browser or directly in your codebase, Keak provides powerful interfaces for creating tests, selecting elements, and generating variants with AI assistance. *** ## Chrome Extension No-code A/B testing directly in your browser. Select elements visually and launch tests without writing any code. ### Core Features #### Page Context Extraction Automatically understand the structure and content of any webpage. - **Semantic Analysis**: AI-powered understanding of page elements - **Element Detection**: Identify interactive and conversion-critical elements - **Content Classification**: Categorize page sections (hero, CTA, forms, etc.) - **Relationship Mapping**: Understand how elements relate to each other 📚 [View Context Extraction →](#) #### Variant Generation Let AI create test variants for you, or customize manually. - **AI-Powered Variants**: Automatically generate alternative versions - **Visual Editor**: Make manual adjustments to generated variants - **Multi-element Tests**: Test changes across multiple page elements - **Preview Mode**: See variants before launching - **Version History**: Track and revert variant changes 📚 [View Variant Generation →](#) #### Element Selection Visual interface for targeting specific page elements. - **Point-and-Click Selection**: Click any element to target it - **CSS Selector Generation**: Automatic selector creation - **XPath Support**: Alternative element targeting methods - **Selector Validation**: Ensure selectors work across page loads - **Multiple Elements**: Target element groups simultaneously 📚 [View Element Selection →](#) #### Launching Tests Deploy your A/B tests directly from the extension. - **One-Click Launch**: Start tests immediately - **Test Configuration**: Set traffic allocation and goals - **Variant Distribution**: Configure how variants are shown - **Goal Assignment**: Define success metrics for the test - **Instant Activation**: Tests go live immediately 📚 [View Test Launching →](#) *** ## Keak Code IDE AI-powered IDE integration for developers who want to test at the codebase level. ### IDE Features #### IDE Setup & Integration Get started with Keak Code in your development environment. - **Installation**: Add Keak Code to your IDE - **Authentication**: Connect to your Keak account - **Project Configuration**: Set up testing for your codebase - **Extension Settings**: Customize IDE behavior - **Keyboard Shortcuts**: Speed up your workflow 📚 [View IDE Setup →](#) #### Codebase Testing Create A/B tests directly from your source code. - **Component Selection**: Target React/Vue/Angular components - **Code Analysis**: Understand component structure and props - **Test Scaffolding**: Generate test boilerplate automatically - **Local Preview**: See variants in your dev environment - **Git Integration**: Track test changes in version control 📚 [View Codebase Testing →](#) #### AI Variant Generation in IDE Generate code-level variants with AI assistance. - **Component Variants**: AI-generated alternative implementations - **Style Variations**: Test different CSS/styling approaches - **Content Alternatives**: Generate copy variations - **Prop Combinations**: Test different component configurations - **Code Quality**: Maintain code standards in generated variants 📚 [View IDE Variant Generation →](#) *** ## Workflow Comparison ### When to Use Chrome Extension - Testing on live websites you may not control - Quick experiments without code deployment - Marketing team-led testing - Visual element changes (text, images, colors) - No-code workflow preference ### When to Use Keak Code IDE - Testing in your own codebase - Component-level experiments - Developer-led testing - Complex functional changes - Version-controlled test variants --- /docs/extension-ide/ide/ # Keak-Code IDE Overview ## What is Keak-Code IDE? Keak-Code is an **Electron-based desktop application** that brings AI-powered A/B testing directly into your development workflow. Unlike the Chrome Extension which manipulates live site DOM, Keak-Code makes changes at the codebase level—editing your actual source files. **Key Distinction**: Keak-Code is not a text editor. It uses Monaco Editor for code viewing, but its primary focus is on **visual element selection** combined with **AI variant generation**. Think of it as a bridge between your local development environment and production experiments. ## Why Keak-Code vs Chrome Extension? | Chrome Extension | Keak-Code IDE | |------------------|---------------| | Live site changes (DOM manipulation) | Codebase changes (source file edits) | | No code access | Full project context | | No build pipeline awareness | Integrated with dev server | | Limited to CSS/HTML changes | Full React/JSX component changes | | Marketing-team friendly | Developer workflow | **Use Keak-Code when:** - You own the codebase and want to test component-level changes - You need full React/JSX transformation capabilities - You want variants version-controlled with your code - You're testing complex functional changes, not just styling ## High-Level Architecture ```mermaid flowchart TB subgraph IDE[Keak-Code IDE - Electron App] PREVIEW[Preview Browser] EDITOR[Monaco Editor] CHAT[CRO Chat Drawer] DEVSERVER[Dev Server Manager] end subgraph Dashboard[Keak Dashboard - Next.js] AUTH[Authentication] PROJECTS[Projects API] KEAFAB_PROXY[KeaFab Proxy] TELEMETRY_API[Telemetry API] end subgraph External[External Services] KEAFAB[KeaFab API] TELEMETRY_DB[Telemetry Database] end PREVIEW -->|WebSocket| DEVSERVER PREVIEW -->|Element Selected| CHAT CHAT -->|Generate Variant| KEAFAB_PROXY KEAFAB_PROXY -->|Proxied Request| KEAFAB IDE -->|Auth Token| AUTH IDE -->|Track Events| TELEMETRY_API TELEMETRY_API --> TELEMETRY_DB ``` ### Note on KeaFab Access The IDE **never calls KeaFab directly**. All AI generation goes through Dashboard API (`/keafab/*` endpoints), which: - **Handles authentication** - No API keys stored in the Electron app - **Enables backend updates** - Update KeaFab without releasing new IDE versions - **Centralizes LLM access** - Single point for all AI provider integrations - **Validates permissions** - Dashboard validates user tokens and quotas This architecture keeps the IDE thin while maintaining security and flexibility. ## Key Features ### 1. Visual Element Selection Click any element in your preview browser to start optimizing it: - **WebSocket communication** with your dev server captures element data - **Automatic source file resolution** via Babel plugin metadata or keakId search - **KeakId injection** provides stable element tracking across code changes - **Self-healing** re-injects keakIds if they're accidentally removed ### 2. AI Variant Generation (via KeaFab) Transform screenshots and element code into CRO-optimized variants: - **Input**: Screenshot + element code + page context + your goal - **Output**: Optimized variant with CRO analysis and expected impact - **Streaming progress** shows real-time updates (router → optimizer → validator) - **Multi-turn conversations** allow iterative refinement per element - **Auto-repair** fixes syntax errors automatically (up to 3 attempts) - **Default prompt**: If no goal provided, uses "Optimize for maximum conversions" 📚 **See**: [Variant Generation](./variant-generation) for detailed workflow | [KeaFab API docs](/docs/ai-infrastructure/keafab/) for AI pipeline ### 3. Lossless Preview System Edit both control and variant versions without losing changes: - **Just-in-time bidirectional swapping** between control ↔ variant - **Edit preservation** for both versions independently - **Single source of truth** on disk at any moment (determined by `activePreview` flag) - **No file watchers** - edits captured only at explicit action time - **Offline-safe** - works even if you edit files in VS Code while IDE is closed **Core concept**: Two versions (control + variant), one disk slot. The system reads from disk before each swap to capture edits, then writes the other version. 📚 **See**: [Preview System](./preview-system) for implementation details ### 4. State Management Robust 3-tier state architecture ensures data durability: - **Tier 1: Zustand Store** - Global UI state, partially persisted to localStorage - **Tier 2: ChatPersistence** - Project-specific history in `.keak/history/history.json` - **Tier 3: React Component State** - Transient state for active sessions - **Snapshots** prevent the variant-as-control bug on app restarts - **Auto-save** (500ms debounced) writes conversation history to disk - **Survives** app restarts, drawer closures, and tab switches 📚 **See**: [State Management](./state-management) for data flow ### 5. Experiment Launch Transform variants into production-ready A/B tests: - **Wrap code** in `` component with control + variant - **Sync to Dashboard** for experiment tracking - **Telemetry tracking** for all generation/preview/apply events - **GitHub integration** for PR creation when selecting winners 📚 **See**: [Experiment Workflow](./experiment-workflow) for end-to-end process ### 6. Multi-Framework Support Works with your existing setup: - **Next.js** (App Router, Pages Router) - **Create React App** - **Vite** - **Static HTML** - **Automatic SDK detection** and installation - **Babel plugin / Webpack loader** for element tagging 📚 **See**: [Setup & Installation](./setup) for getting started ## Documentation Structure This documentation is organized for **internal engineering onboarding**. The goal is to understand Keak-Code's implementation without diving into source files directly. ### Core Documentation - **[Setup & Installation](./setup)** - Get Keak-Code running on your project - **[Architecture](./architecture)** - Technical deep dive: components, services, hooks - **[Core Concepts](./core-concepts)** - Mental models: keakId, snapshots, swapping ### Implementation Details - **[Element Selection & Tracking](./element-tracking)** - WebSocket protocol, Babel plugin, boundary detection - **[Preview System](./preview-system)** - Just-in-time swapping algorithm and FileStateManager - **[State Management](./state-management)** - 3-tier architecture, persistence, hydration - **[Variant Generation](./variant-generation)** - AI generation workflow, auto-repair, prompt suggestions ### Workflows & Integration - **[Experiment Workflow](./experiment-workflow)** - Complete user journey from selection to launch - **[Babel Integration](./babel-integration)** - Element tagging, source mapping - **[Telemetry](./telemetry)** - What's tracked, variant lifecycle states - **[Git Integration](./git-integration)** - PR creation, winner selection ### Reference - **[Advanced Topics](./advanced)** - Deep dives, edge cases, optimizations - **[Troubleshooting](./troubleshooting)** - Common issues and solutions - **[API Reference](./api-reference)** - Services, hooks, configuration ## Quick Start 1. **Open a project** in Keak-Code (or import from GitHub) 2. **SDK auto-installs** - Babel plugin injects element tracking attributes 3. **Dev server starts** - Your app loads in the preview panel 4. **Click an element** - CRO drawer opens with AI suggestions 5. **Generate variant** - Enter your goal or use AI-suggested prompts 6. **Preview changes** - Toggle between control and variant 7. **Apply variant** - Wrap in experiment component and launch Ready to dive deeper? Start with [Architecture](./architecture) for the technical foundation, or jump to [Setup](./setup) to get your project configured. ## Related Documentation - **[KeaFab API](/docs/ai-infrastructure/keafab/)** - AI variant generation pipeline - **[Telemetry Database](/docs/ai-infrastructure/database/telemetry/keak-code)** - Event tracking schema - **[Chrome Extension](/docs/extension/)** - Browser-based A/B testing alternative --- /docs/extension-ide/ide/advanced # Advanced Topics Deep implementation details, edge cases, and optimization techniques for engineers working with or extending Keak-Code. *** ## FileStateManager Deep Dive ### Internal Data Structures **Location**: `services/fileStateManager.ts` ```typescript class FileStateManager { // Primary storage: keakId → element snapshot private elementSnapshots: Map = new Map(); // Performance cache: filePath → latest file content private currentContent: Map = new Map(); // Boundary cache: keakId → last computed boundaries private boundaryCache: Map = new Map(); } interface ElementSnapshot { keakId: string; filePath: string; originalCode: string; // Control version variantCode: string | null; // Treatment version activePreview: boolean; // Which version is on disk boundaries?: ElementBoundaries; // Cached for performance lastModified?: number; // For cache invalidation } ``` **Why Map instead of Object?** - ✅ **Type-safe keys** - keakId is guaranteed string - ✅ **Fast lookups** - O(1) average case - ✅ **Iteration support** - Can easily iterate all snapshots - ✅ **Size tracking** - `map.size` for debugging ### Memory Management **Snapshot cleanup** (when conversation cleared): ```typescript function clearElement(keakId: string): void { // Remove from all caches this.elementSnapshots.delete(keakId); this.boundaryCache.delete(keakId); // Keep currentContent (might be used by other elements in same file) } ``` **File content cache invalidation**: ```typescript function invalidateContentCache(filePath: string): void { // Force re-read on next operation this.currentContent.delete(filePath); } ``` **When to invalidate**: - File modified externally (detected via timestamp check) - Dev server restart - User switches branches ### Edge Case: Multiple Elements in Same File **Scenario**: Hero.tsx has 5 tracked elements. **Problem**: Each preview/hide reads the file. Can we optimize? **Solution**: Batch operations + shared cache. ```typescript class FileStateManager { // Track which elements are in which files private fileElements: Map> = new Map(); // filePath → keakIds registerElement(keakId: string, filePath: string, fileContent: string) { // Track file → element relationship if (!this.fileElements.has(filePath)) { this.fileElements.set(filePath, new Set()); } this.fileElements.get(filePath).add(keakId); // ... rest of registration } // Batch preview multiple elements in same file async applyMultiplePreviews( filePath: string, previews: Array<{ keakId: string; variantCode: string }> ): Promise { // Read file once let fileContent = await readFile(filePath); // Apply all previews sequentially for (const { keakId, variantCode } of previews) { const result = this.applyPreview(filePath, keakId, fileContent, variantCode); fileContent = result.newContent; // Use updated content for next preview } // Write once await writeFile(filePath, fileContent); return fileContent; } } ``` **Optimization**: 5 previews in same file = 1 read + 1 write (instead of 5 reads + 5 writes). *** ## Build Error Handling ### Detecting Build Errors **Dev server output monitoring**: ```typescript // Monitor Terminal output onDevServerOutput((line: string) => { // Detect compilation errors if (line.includes('ERROR') || line.includes('Failed to compile')) { const error = parseCompilationError(line); handleBuildError(error); } }); function parseCompilationError(output: string): BuildError { // Extract file, line, message const match = output.match(/(.+):(\d+):(\d+) - error (.+)/); if (match) { return { file: match[1], line: parseInt(match[2]), column: parseInt(match[3]), message: match[4] }; } return { message: output }; // Unparseable error } ``` ### Auto-Repair Mechanism **When variant causes build error**: ```typescript async function handleBuildError(error: BuildError): Promise { // Check if error relates to a tracked element const keakId = findKeakIdInFile(error.file); if (!keakId) return; // Not our element const snapshot = fileStateManager.getSnapshot(keakId); if (!snapshot || !snapshot.activePreview) return; // No variant active console.warn('[Build Error] Variant caused compilation error:', error); // Attempt GPT-based repair const repaired = await gptBuildErrorRepair( snapshot.variantCode, error.message ); if (repaired) { // Apply repaired code await fileStateManager.applyPreview( snapshot.filePath, keakId, await readFile(snapshot.filePath), repaired ); // Update variant in state updateVariantCode(variantId, repaired); console.log('[Build Error] Auto-repaired successfully'); } else { // Repair failed - show modal showBuildErrorModal(error, snapshot); } } ``` **GPT repair prompt**: ```typescript async function gptBuildErrorRepair( code: string, errorMessage: string ): Promise { const prompt = `Fix this TypeScript/JSX compilation error: Error: ${errorMessage} Code: \`\`\`tsx ${code} \`\`\` Return ONLY the fixed code with no explanations.`; const response = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [ { role: 'system', content: 'You are a TypeScript/JSX compilation error fixer.' }, { role: 'user', content: prompt } ], temperature: 0, max_tokens: 3000 }); return response.choices[0].message.content.trim(); } ``` **Success rate**: ~70% of build errors auto-fixed. ### Auto-Issue Reporting **When auto-repair fails**, offer to report the issue: ```typescript function showBuildErrorModal(error: BuildError, snapshot: ElementSnapshot) { Modal.show({ title: 'Build Error Detected', content: ` The variant caused a compilation error: ${error.message} Auto-repair attempted but failed. Would you like to: - Report this issue automatically (helps improve Keak) - Manually fix the code - Hide the preview (revert to control) `, actions: [ { label: 'Report Issue', onClick: async () => { await reportIssueToGitHub({ title: `Build error: ${error.message.substring(0, 50)}`, body: ` **Error**: ${error.message} **Variant Code**: \`\`\`tsx ${snapshot.variantCode} \`\`\` **User Prompt**: ${userGoal} **Generated by**: KeaFab ${keafabVersion} `, labels: ['auto-reported', 'build-error'] }); } }, { label: 'Hide Preview', onClick: async () => { await fileStateManager.hidePreview( snapshot.filePath, snapshot.keakId, await readFile(snapshot.filePath) ); } } ] }); } ``` *** ## Multi-Framework Support ### Framework Detection Logic **Location**: `services/projectSetup.ts` ```typescript async function detectFramework(projectRoot: string): Promise { const hasFile = (name: string) => fs.existsSync(path.join(projectRoot, name)); const hasDir = (name: string) => fs.existsSync(path.join(projectRoot, name)) && fs.statSync(path.join(projectRoot, name)).isDirectory(); // Next.js detection if (hasFile('next.config.js') || hasFile('next.config.ts') || hasFile('next.config.mjs')) { // Distinguish App Router vs Pages Router if (hasDir('app')) { return { type: 'nextjs', router: 'app' }; } else if (hasDir('pages')) { return { type: 'nextjs', router: 'pages' }; } return { type: 'nextjs', router: 'unknown' }; } // Vite detection if (hasFile('vite.config.ts') || hasFile('vite.config.js')) { return { type: 'vite' }; } // Create React App detection const packageJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf-8')); if (packageJson.dependencies?.['react-scripts']) { return { type: 'create-react-app' }; } // Remix detection if (hasFile('remix.config.js')) { return { type: 'remix' }; } // Astro detection if (hasFile('astro.config.mjs')) { return { type: 'astro' }; } // Static HTML detection if (hasFile('index.html') && !hasFile('package.json')) { return { type: 'static-html' }; } return { type: 'unknown' }; } ``` ### Adding a New Framework **Steps to support a new framework** (e.g., Solid.js): 1. **Add detection logic**: ```typescript if (hasFile('vite.config.ts') && packageJson.dependencies?.['solid-js']) { return { type: 'solid' }; } ``` 2. **Add setup handler**: ```typescript async function setupSolid(projectRoot: string): Promise { await installSDK(projectRoot); // Configure Vite for Solid + Babel plugin await configureSolidBabelPlugin(projectRoot); await injectKeakProviderToApp(projectRoot, 'src/App.tsx'); } ``` 3. **Add to switch statement**: ```typescript switch (framework.type) { case 'nextjs': await setupNextJs(...); break; case 'vite': await setupVite(...); break; case 'solid': await setupSolid(...); break; // New! } ``` **No changes needed** in: - Element selection (framework-agnostic) - Variant generation (JSX is JSX) - Preview system (file swapping works anywhere) *** ## WebSocket Protocol ### Connection Establishment **Preview browser** (injected script): ```typescript const ws = new WebSocket('ws://localhost:9000'); ws.onopen = () => { console.log('[Keak] WebSocket connected to IDE'); }; ws.onerror = (error) => { console.error('[Keak] WebSocket error:', error); }; ws.onclose = () => { console.log('[Keak] WebSocket disconnected, retrying...'); setTimeout(() => connectWebSocket(), 1000); // Retry after 1s }; ``` **IDE** (App.tsx): ```typescript const [wsConnected, setWsConnected] = useState(false); useEffect(() => { const ws = new WebSocket(`ws://localhost:9000`); ws.onopen = () => { setWsConnected(true); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); handleWebSocketMessage(message); }; ws.onclose = () => { setWsConnected(false); }; return () => ws.close(); }, []); ``` ### Message Types **Element Selected**: ```typescript { type: 'element-selected', data: { outerHTML: string; innerHTML: string; tagName: string; className: string; textContent: string; attributes: Record; boundingRect: { top: number; left: number; width: number; height: number; }; } } ``` **Variant Applied** (IDE → Preview): ```typescript { type: 'variant-applied', data: { keakId: string; experimentKey: string; } } ``` **Heartbeat**: ```typescript // IDE → Preview (every 30s) { type: 'ping' } // Preview → IDE { type: 'pong' } ``` ### Reconnection Strategy **Exponential backoff**: ```typescript let reconnectAttempts = 0; const maxReconnectAttempts = 10; function connectWebSocket() { const ws = new WebSocket('ws://localhost:9000'); ws.onclose = () => { if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); console.log(`[Keak] Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`); setTimeout(connectWebSocket, delay); } else { console.error('[Keak] Max reconnection attempts reached'); showReconnectionFailedModal(); } }; ws.onopen = () => { reconnectAttempts = 0; // Reset on success }; } ``` **Delays**: 1s, 2s, 4s, 8s, 16s, 30s (capped) *** ## Performance Optimization ### 1. Debounced Persistence **Used in**: ChatPersistence auto-save ```typescript useEffect(() => { const timeoutId = setTimeout(() => { chatPersistence.saveChat(keakId, state); }, 500); return () => clearTimeout(timeoutId); }, [state]); ``` **Benefit**: 10 rapid state changes → 1 disk write (90% reduction). ### 2. Boundary Caching **Problem**: `findElementBoundaries()` is expensive (~50ms). **Solution**: Cache after first computation. ```typescript function applyPreview(...) { const boundaries = snapshot.boundaries || findElementBoundaries(lines, keakId); snapshot.boundaries = boundaries; // Cache // ... } ``` **Cache invalidation** (when file changes significantly): ```typescript if (Math.abs(newLineCount - snapshot.lastLineCount) > 10) { delete snapshot.boundaries; // Force recomputation } ``` ### 3. In-Memory Content Cache **Avoid redundant file reads**: ```typescript class FileStateManager { private currentContent = new Map(); async readFileWithCache(filePath: string): Promise { // Try cache first if (this.currentContent.has(filePath)) { return this.currentContent.get(filePath); } // Cache miss - read from disk const content = await electronAPI.readFile(filePath); this.currentContent.set(filePath, content); return content; } } ``` **Trade-off**: Cache can become stale. Invalidate on file writes. ### 4. Smart Validation Skipping **KeaFab heuristic check** before calling Validator: ````python def _needs_validation(original: str, variant: str) -> bool: """Skip validation if variant looks clean.""" # Check 1: No markdown code fences if '```' in variant: return True # Needs validation # Check 2: data-keak-id appears exactly once keak_id_count = variant.count('data-keak-id=') if keak_id_count != 1: return True # Needs validation # Check 3: All data-* attributes present original_attrs = re.findall(r'data-\w+', original) for attr in original_attrs: if attr not in variant: return True # Missing attribute # Check 4: Balanced tags if not is_balanced(variant): return True # Needs validation return False # Looks clean, skip validation ```` **Saves**: ~2-5 seconds per generation when validation is skipped. *** ## Security Considerations ### File System Access Control **Electron IPC validation**: ```typescript ipcMain.handle('read-file', async (event, filePath: string) => { const { projectRoot } = getState(); // Normalize paths const normalizedPath = path.normalize(filePath); const normalizedRoot = path.normalize(projectRoot); // Check 1: Within project root if (!normalizedPath.startsWith(normalizedRoot)) { throw new Error('Access denied: Path outside project root'); } // Check 2: No path traversal if (filePath.includes('..')) { throw new Error('Access denied: Path traversal detected'); } // Check 3: Not a sensitive file const basename = path.basename(filePath); const sensitiveFiles = ['.env', '.env.local', 'id_rsa', 'credentials.json']; if (sensitiveFiles.includes(basename)) { throw new Error('Access denied: Sensitive file'); } return fs.readFileSync(filePath, 'utf-8'); }); ``` ### Token Management **Using keytar** (OS keychain): ```typescript import keytar from 'keytar'; async function storeAuthToken(token: string): Promise { await keytar.setPassword('keak-code', 'auth-token', token); } async function getAuthToken(): Promise { return await keytar.getPassword('keak-code', 'auth-token'); } ``` **Benefits**: - ✅ **Encrypted** - OS-level encryption - ✅ **Secure** - Not extractable from app - ✅ **Per-user** - Different tokens per OS user ### WebSocket Authentication **Handshake token**: ```typescript // IDE generates token on startup const handshakeToken = crypto.randomBytes(32).toString('hex'); // Inject token into preview script const script = ` const ws = new WebSocket('ws://localhost:9000'); ws.onopen = () => { ws.send(JSON.stringify({ type: 'auth', token: '${handshakeToken}' })); }; `; // IDE validates token ws.on('message', (data) => { const msg = JSON.parse(data); if (msg.type === 'auth') { if (msg.token === handshakeToken) { ws.authenticated = true; } else { ws.close(1008, 'Invalid token'); } } }); ``` **Prevents**: Unauthorized connections from other processes. *** ## Concurrent Variant Generation ### The Challenge User generates variants for **multiple elements simultaneously**. **State management** across parallel generations: ```typescript const [activeGenerations, setActiveGenerations] = useState>(new Map()); interface GenerationState { keakId: string; generationId: string; startedAt: number; progress: string; } function handleGenerate(keakId: string, goal: string) { const generationId = uuid(); // Add to active generations setActiveGenerations(prev => new Map(prev).set(keakId, { keakId, generationId, startedAt: Date.now(), progress: 'Starting...' })); // Generate (async) generator.generate(goal).then(result => { // Remove from active setActiveGenerations(prev => { const next = new Map(prev); next.delete(keakId); return next; }); handleGenerationComplete(keakId, result); }); } ``` **UI progress indicator**: ```typescript
{Array.from(activeGenerations.values()).map(gen => (
{gen.keakId}: {gen.progress}
))}
``` *** ## Experiment Wrapper Generation ### AST-Based Approach **Using ts-morph** for reliable code generation: ```typescript import { Project, SyntaxKind } from 'ts-morph'; function wrapInExperiment( sourceCode: string, keakId: string, controlCode: string, variantCode: string, experimentKey: string ): string { const project = new Project(); const sourceFile = project.createSourceFile('temp.tsx', sourceCode); // Add import if missing const hasImport = sourceFile.getImportDeclaration( decl => decl.getModuleSpecifierValue() === '@keak/sdk' ); if (!hasImport) { sourceFile.addImportDeclaration({ moduleSpecifier: '@keak/sdk', namedImports: ['Experiment', 'Variant'] }); } // Find element with keakId const elements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement); const target = elements.find(el => { const attrs = el.getOpeningElement().getAttributes(); return attrs.some(attr => attr.getName() === 'data-keak-id' && attr.getInitializer()?.getText() === `"${keakId}"` ); }); if (!target) throw new Error(`Element ${keakId} not found`); // Replace with experiment wrapper const experimentCode = ` ${controlCode} ${variantCode} `; target.replaceWithText(experimentCode); return sourceFile.getFullText(); } ``` **Fallback**: Regex-based replacement if ts-morph fails. *** ## State Debugging Tools ### Zustand DevTools ```typescript import { devtools } from 'zustand/middleware'; export const useKeakStore = create()( devtools( persist( (set, get) => ({ /* store */ }), { name: 'keak-chat-store' } ), { name: 'KeakStore' } ) ); ``` **Usage**: Redux DevTools extension → See "KeakStore" with time-travel debugging. ### Debug Window Object ```typescript if (process.env.NODE_ENV === 'development') { window.__KEAK_DEBUG__ = { fileStateManager: getFileStateManager(), chatPersistence: getChatPersistence(), getSnapshot: (keakId) => getFileStateManager().getSnapshot(keakId), getAllSnapshots: () => Array.from(getFileStateManager()['elementSnapshots'].entries()) }; } ``` **Console usage**: ```javascript window.__KEAK_DEBUG__.getSnapshot('hero-cta-button') window.__KEAK_DEBUG__.getAllSnapshots() ``` *** ## Performance Benchmarks | Operation | Time | Bottleneck | Optimization | |-----------|------|------------|--------------| | Element selection | 100-300ms | WebSocket + file resolution | Babel plugin (10x faster) | | Boundary detection | 10-50ms | JSX parsing | Boundary caching (∞x faster) | | File swap | 20-80ms | File I/O | In-memory cache | | ChatPersistence save | 20-100ms | JSON stringify + write | Debouncing (90% reduction) | | Variant generation | 10-30s | KeaFab API (network + LLM) | None (external) | *** ## Related Documentation - **[Architecture](./architecture)** - System overview - **[Preview System](./preview-system)** - FileStateManager details - **[State Management](./state-management)** - Persistence architecture - **[Element Tracking](./element-tracking)** - WebSocket protocol - **[Troubleshooting](./troubleshooting)** - Common issues --- /docs/extension-ide/ide/api-reference # API Reference **Coming Soon** Reference documentation for key APIs and services. ## Electron API File system and system operations: - `electronAPI.readFile(path)` - Read file - `electronAPI.writeFile(path, content)` - Write file - `electronAPI.ensureDir(path)` - Create directory - `electronAPI.startDevServer(config)` - Start dev server - `electronAPI.stopDevServer()` - Stop dev server - Git operations - Process management ## Services API ### variantApplier Location: `services/variantApplier.ts` ```typescript previewVariant(sourceMapping, variantCode, keakId, onWriteFile) hidePreview(sourceMapping, keakId, onWriteFile) applyVariant(sourceMapping, variantCode, keakId) ``` ### fileStateManager Location: `services/fileStateManager.ts` ```typescript registerElement(keakId, filePath, fileContent) applyPreview(filePath, keakId, fileContent, variantCode) hidePreview(filePath, keakId, fileContent) getCodesForExperiment(filePath, keakId, fileContent) restoreSnapshot(keakId, filePath, snapshot) getSnapshot(keakId) ``` ### chatPersistence Location: `services/chatPersistence.ts` ```typescript loadChat(keakId) saveChat(keakId, state) loadHistory() clearChat(keakId) ``` ### keafabApi Location: `services/keafabApi.ts` ```typescript generateVariant(request) generateVariantStreaming(request, onProgress) ``` ### keafabCodeRepair Location: `services/ai/keafabCodeRepair.ts` ```typescript repairCodeWithKeafab( originalSourceFile: string, currentSourceFile: string, variantCode: string, buildError: string, keakId: string ): Promise extractVariantByKeakId(sourceCode: string, keakId: string): string | null ``` **RepairResult**: ```typescript interface RepairResult { fixed: boolean; code: string; // Extracted variant code fullRepairedFile?: string; // Full repaired file for direct write error?: string; usage?: object; // Token usage from API } ``` ### autoIssueReporter Location: `services/autoIssueReporter.ts` ```typescript parseBuildError(errorMessage: string): ParsedBuildError findRecentVariantForFile(filePath: string, timestamp: number): RecentVariant | null submitAutoReport(report: AutoReport, token?: string, email?: string): Promise shouldAutoReport(context: AutoReportContext): boolean ``` ## Custom Hooks ### useSimpleVariantApplier ```typescript const { preview, hide, apply, state } = useSimpleVariantApplier(...) ``` ### useKeafabGenerator ```typescript const { generate, isGenerating, progress } = useKeafabGenerator(...) ``` ### useCROActions ```typescript const { handleGenerate, handlePreview, handleApply } = useCROActions(...) ``` ### use-prompt-suggestions ```typescript const { suggestions, isLoading } = usePromptSuggestions({ enabled, elementId, selectedComponent, sourceCode }) ``` ### useCodeRepair ```typescript const { // State hasBuildError, buildErrorMessage, devServerPort, showBuildErrorReportedModal, buildErrorReportedData, isRepairing, // Actions handleDevServerError, clearBuildError, handleVariantBuildError, closeBuildErrorReportedModal, handleRegenerate, handleEditPrompt, } = useCodeRepair({ token, userEmail }); ``` **Props**: - `token` - Auth token for API calls - `userEmail` - User email for auto-reporting **State**: - `hasBuildError` - Whether build error modal should show - `buildErrorMessage` - Full error message text - `devServerPort` - Dev server port number (for troubleshooting) - `isRepairing` - Whether auto-repair is in progress (shows blocking modal) - `showBuildErrorReportedModal` - Whether error reported modal should show - `buildErrorReportedData` - Error details for reported modal **Actions**: - `handleDevServerError(cleanData, callbacks)` - Main error detection and routing - `handleVariantBuildError(errorMessage)` - Auto-repair for variant errors - `clearBuildError()` - Reset error state - `closeBuildErrorReportedModal()` - Dismiss error reported modal - `handleRegenerate()` - Regenerate variant after error - `handleEditPrompt()` - Allow user to edit prompt after error **Error Patterns Detected**: - SWC syntax errors (`/Error:.*Unexpected token/i`) - Babel syntax errors (`/SyntaxError.*Unexpected/i`) - Build failures (`/Failed to compile/i`) - Module errors (`/Cannot find module/i`) - Variant-related errors (`/Unexpected eof/i`, `/Unterminated/i`) **See also**: - [KeaFab Repair API](/docs/ai-infrastructure/keafab/endpoints#code-repair-endpoint) - Repair endpoint documentation - [Repair Integration Patterns](/docs/ai-infrastructure/keafab/client-integration#keafabcoderepair-service) - How repair integrates with IDE ## Configuration ### Project Config `.keak/project.json`: ```json { "projectId": "uuid", "projectName": "My App" } ``` ### Chat History `.keak/history/history.json` - See [State Management](./state-management) ## Environment Variables - `KEAK_BACKEND_URL` - Dashboard API URL - Development vs production modes --- /docs/extension-ide/ide/architecture # Architecture A deep dive into Keak-Code's technical architecture, component structure, and design decisions. ## Application Structure ### Entry Point **File**: `keak-code/src/renderer/src/App.tsx` (~2400 lines) The App component is the main entry point that orchestrates the entire IDE experience. It uses **conditional rendering** based on authentication and project state rather than traditional routing. ### Render Logic ```typescript function App() { const { isAuthenticated } = useAuth(); const { root } = useProject(); if (!isAuthenticated) { return ; } if (!root) { return ; // Import from GitHub or open folder } return ; // Three-panel IDE interface } ``` ### Three-Panel Layout ``` ┌───────────────────────────────────────────────────────────────┐ │ Top Bar: Logo | Project Name | Code/Preview Toggle | User │ ├─────────────┬──────────────────────────┬──────────────────────┤ │ Sidebar │ Center Panel │ Right Panel │ │ (260px) │ (Flexible) │ (30% min) │ │ │ │ │ │ ┌─────────┐ │ ┌────────────────────┐ │ ┌─────────────────┐ │ │ │ Files │ │ │ Code Mode: │ │ │ Tab Bar: │ │ │ │ Search │ │ │ Monaco Editor │ │ │ [Chat] [Expts] │ │ │ └─────────┘ │ │ │ │ │ [Events] [Setup]│ │ │ │ └────────────────────┘ │ │ [Terminal] │ │ │ File Tree │ OR │ └─────────────────┘ │ │ │ ┌────────────────────┐ │ │ │ (Collapsible│ │ Preview Mode: │ │ Active Tab Content: │ │ in preview)│ │ Embedded Browser │ │ • CRODrawer │ │ │ │ with dev server │ │ • ExperimentPanel │ │ │ └────────────────────┘ │ • EventsPanel │ │ │ │ • SetupPanel │ │ │ │ • Terminal │ └─────────────┴──────────────────────────┴──────────────────────┘ ``` **Panel Management** (using `react-resizable-panels`): - Sidebar: Fixed 260px, collapsible in preview mode - Center: Flexible width, exclusive code/preview toggle - Right Panel: Minimum 30% width, draggable boundary *** ## Technology Stack | Layer | Technology | Purpose | Why This Choice | |-------|-----------|---------|-----------------| | **Desktop Shell** | Electron | File system access, dev server control, native OS integration | Required for local file access and dev server management | | **UI Framework** | React + TypeScript | Component-based UI | Type safety, component reusability, strong ecosystem | | **Code Editor** | Monaco Editor | Syntax highlighting, read-only viewing | Industry standard (VS Code uses it), lightweight | | **Layout** | react-resizable-panels | Draggable panel boundaries | Better UX than fixed panels, simple API | | **State Management** | Zustand | Global stores | Simpler than Redux, built-in persistence, excellent TypeScript support | | **HTTP Client** | fetch API | Dashboard and KeaFab API calls | Native, no extra dependencies | | **WebSocket** | ws | Dev server ↔ preview communication | Real-time element selection | | **Analytics** | PostHog | Feature usage tracking | User behavior insights, funnel analysis | *** ## Component Hierarchy ``` App.tsx (main entry, ~2400 lines) │ ├── LoginScreen (not authenticated) │ ├── AuthForm │ └── OAuthButtons │ ├── WelcomeScreen (no project open) │ ├── ImportFromGitHub │ └── OpenLocalFolder │ └── MainLayout (authenticated + project open) │ ├── TopBar │ ├── KeakLogo │ ├── ProjectName │ ├── CodePreviewToggle (switch between modes) │ ├── UserEmail │ └── SignOutButton │ ├── ResizablePanelGroup (3 panels) │ │ │ ├── LeftSidebar (ResizablePanel, size=260px) │ │ ├── TabBar (Files | Search) │ │ ├── FileTree (when sidebarTab='files') │ │ │ ├── DirectoryNode (recursive) │ │ │ └── FileNode (onClick → opens in editor) │ │ └── FindInFiles (when sidebarTab='search') │ │ ├── SearchInput │ │ └── SearchResults │ │ │ ├── CenterPanel (ResizablePanel, flexible) │ │ ├── CodeEditor (when !showPreview) │ │ │ ├── Monaco (syntax highlighting) │ │ │ ├── HighlightedLine (selected element) │ │ │ └── AutoSave (Cmd+S) │ │ └── PreviewPanel (when showPreview) │ │ ├── IFrame (dev server URL) │ │ ├── DeviceToggle (mobile/tablet/desktop/responsive) │ │ ├── KeakToolbar (injected into iframe) │ │ └── WebSocketHandler (element selection) │ │ │ └── RightPanel (ResizablePanel, minSize=30%) │ ├── TabBar │ │ ├── ChatTab │ │ ├── ExperimentsTab │ │ ├── EventsTab │ │ ├── SetupTab │ │ └── TerminalTab │ │ │ ├── CRODrawer (when activeRightTab='chat') │ │ ├── DrawerHeader │ │ ├── ChatTabBar (multi-element tabs) │ │ │ ├── TabChip (per keakId) │ │ │ └── CloseButton │ │ ├── ViewToggle (chat | editor | history) │ │ ├── ChatView (when activeView='chat') │ │ │ ├── ChatMessages │ │ │ │ ├── UserMessage │ │ │ │ └── AssistantMessage │ │ │ ├── VariantsList │ │ │ │ ├── VariantCard │ │ │ │ │ ├── PreviewIndicator │ │ │ │ │ ├── AnalysisText │ │ │ │ │ ├── ChangesText │ │ │ │ │ ├── ValidationIssues │ │ │ │ │ ├── FeedbackButtons │ │ │ │ │ └── ActionButtons │ │ │ │ │ ├── PreviewButton │ │ │ │ │ ├── ApplyButton │ │ │ │ │ ├── RegenerateButton │ │ │ │ │ └── JumpToCodeButton │ │ │ ├── SuggestedPrompts │ │ │ └── InputArea │ │ │ ├── Textarea │ │ │ ├── ImageUpload (max 3) │ │ │ └── GenerateButton │ │ ├── VisualEditorPanel (when activeView='editor') │ │ └── HistoryPanel (when activeView='history') │ │ ├── HistoryList │ │ └── HistoryItem (onClick → load conversation) │ │ │ ├── ExperimentPanel (when activeRightTab='experiments') │ │ ├── ExperimentsList │ │ ├── CreateExperimentButton │ │ └── ExperimentGroupingDialog │ │ │ ├── EventsPanel (when activeRightTab='events') │ │ ├── EventScanner │ │ └── EventsList │ │ │ ├── SetupPanel (when activeRightTab='setup') │ │ ├── SDKInstallation │ │ ├── BabelPluginSetup │ │ └── ProjectConfiguration │ │ │ └── InteractiveTerminal (when activeRightTab='terminal') │ └── DevServerOutput ``` *** ## Service Layer Architecture Services are **pure TypeScript classes/functions** in `src/renderer/src/services/` that handle core business logic. ### Services Overview | Service | File | Lines | Purpose | Key Methods | |---------|------|-------|---------|-------------| | **Variant Applier** | `variantApplier.ts` | ~200 | High-level preview/apply operations | `previewVariant()`, `hidePreview()`, `applyVariant()` | | **File State Manager** | `fileStateManager.ts` | ~300 | Control ↔ variant swapping, snapshot management | `applyPreview()`, `hidePreview()`, `registerElement()`, `restoreSnapshot()` | | **Chat Persistence** | `chatPersistence.ts` | ~150 | `.keak/history/history.json` read/write | `loadChat()`, `saveChat()`, `loadHistory()`, `clearChat()` | | **KeaFab API** | `keafabApi.ts` | ~250 | Dashboard `/keafab/*` endpoint client | `generateVariant()`, `generateVariantStreaming()`, `suggest()` | | **Telemetry Service** | `telemetryService.ts` | ~400 | Dashboard `/telemetry/*` endpoint client | `trackGenerationStart()`, `trackVariantPreview()`, `trackVariantAccept()` | | **Project Setup** | `projectSetup.ts` | ~1500 | SDK installation, Babel plugin injection | `checkAndSetupKeakSDK()`, `installSDK()`, `injectBabelPlugin()` | | **Experiment Scanner** | `experimentScanner.ts` | ~200 | Find existing `` components | `scanExperiments()`, `extractExperimentData()` | | **Event Scanner** | `eventScanner.ts` | ~300 | Extract analytics events from code | `scanEvents()`, `extractKeakTrack()` | | **Keafab Code Repair** | `ai/keafabCodeRepair.ts` | ~250 | Auto-repair syntax errors in variants | `repairCodeWithKeafab()`, `extractVariantByKeakId()` | | **Auto Issue Reporter** | `autoIssueReporter.ts` | ~200 | Track and report build errors | `parseBuildError()`, `findRecentVariantForFile()`, `submitAutoReport()` | ### Code Repair Architecture **Unified Error Handling System** (`hooks/use-code-repair.ts`, ~700 lines) Keak-Code features an intelligent error detection and auto-repair system that catches build errors and automatically fixes them using the Keafab AI repair API. #### Two Repair Paths ``` ┌─────────────────────────────────────────────────────────────┐ │ Error Detection │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Preview-Time Repair Post-Build Repair │ │ (use-variant-applier.ts) (use-code-repair.ts) │ │ │ │ ┌─────────────────┐ ┌───────────────────┐ │ │ │ Preview variant │ │ Dev server output │ │ │ │ JSX validation │ │ Terminal monitor │ │ │ └────────┬────────┘ └────────┬──────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────┐ ┌───────────────────┐ │ │ │ Syntax error? │ │ Build error? │ │ │ └────────┬────────┘ └────────┬──────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Keafab Repair API │ │ │ │ /keafab/repair (via Dashboard proxy) │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ ``` **Preview-Time Repair**: - Happens during `previewWithRepair()` call - Validates JSX before writing to disk - Returns extracted variant code - Updates variant state if repaired **Post-Build Repair**: - Monitors terminal output for build errors - Matches errors to recent variant applications - **On success**: Writes full repaired file directly to disk, restores preview state - **On failure**: Automatically reverts to original code, clears preview state, restores dev server #### Error Pattern Detection The `useCodeRepair` hook classifies errors using regex patterns: ```typescript // SWC syntax errors (Next.js 13+) /Error:.*Unexpected token/i // Babel/Webpack syntax errors /SyntaxError.*Unexpected/i // Build failures /Failed to compile/i /Module build failed/i // Variant-related patterns /Unexpected eof/i /Expression expected/i /Unterminated/i ``` #### Concurrency Control Uses **ref + state pattern** to prevent race conditions: ```typescript // Ref for synchronous concurrency guard const repairInProgressRef = useRef(false); // State for UI rendering const [isRepairing, setIsRepairing] = useState(false); // Guard check if (repairInProgressRef.current) { return; // Skip concurrent repair } repairInProgressRef.current = true; setIsRepairing(true); ``` #### Blocking Repair Modal When repair is triggered: 1. **Blocking modal shown** - prevents user interaction 2. **Terminal tab NOT switched** - preserves conversation context 3. **Repair runs automatically** - up to 1 attempt via Keafab 4. **On success**: Modal dismisses, preview state restored, repaired variant on disk 5. **On failure**: - Original code automatically restored to disk - Preview state cleared (no longer showing variant) - Dev server recovers (build error cleared) - Auto-report submitted - Error modal shown with regenerate options #### Auto-Reporting Integration Failed repairs automatically submit reports to GitHub: ```typescript if (shouldAutoReport(autoReportContext)) { await submitAutoReport({ trigger: 'Variant Generation Build Error (Auto-repair failed)', errorMessage, variantCode, originalCode, keakId, variantId }, token, userEmail); } ``` **See also**: - [KeaFab Repair Endpoint](/docs/ai-infrastructure/keafab/endpoints#code-repair-endpoint) - API documentation - [KeaFab Client Integration](/docs/ai-infrastructure/keafab/client-integration#keafabcoderepair-service) - Integration patterns ### Service Dependency Graph ``` ┌─────────────────────────────────────────────────────────────┐ │ App.tsx │ └────────────────────────┬────────────────────────────────────┘ │ uses hooks ▼ ┌─────────────────────────────────────────────────────────────┐ │ Custom Hooks Layer │ │ useKeafabGenerator, useSimpleVariantApplier, useCROActions │ └────────────────────────┬────────────────────────────────────┘ │ calls services ▼ ┌─────────────────────────────────────────────────────────────┐ │ Service Layer │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ variantApplier │───>│ fileStateManager │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ │ │ ├──────────────────────┐│ │ │ ▼ ▼▼ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ keafabApi │ │ chatPersistence │ │ │ │ keafabRepair │ │ autoIssueReporter│ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ telemetryService │ │ elementBoundaries│ │ │ └──────────────────┘ └──────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ uses ▼ ┌─────────────────────────────────────────────────────────────┐ │ Electron API │ │ readFile, writeFile, startDevServer, git operations │ └─────────────────────────────────────────────────────────────┘ ``` **Dependency principles**: - **Hooks** orchestrate multi-service workflows - **Services** are pure logic, no React dependencies - **Electron API** provides system-level operations - **No circular dependencies** (enforced by TypeScript) *** ## Custom Hooks (Orchestration Layer) Hooks are **React-specific** orchestrators in `src/renderer/src/hooks/` that compose multiple services. ### Hooks Overview | Hook | File | Purpose | Services Used | State Managed | |------|------|---------|---------------|---------------| | `useElementSourceResolver` | `use-element-source-resolver.ts` | Map element → source file | Babel metadata parser, file search | Source file path, line/column | | `useDevServerHandlers` | `use-dev-server-handlers.ts` | Start/stop/configure dev server | Electron IPC | Dev server status, URL, port | | `useSetupHandlers` | `use-setup-handlers.ts` | SDK installation, dependencies | projectSetup service | Installation status, errors | | `useProjectHandlers` | `use-project-handlers.ts` | File operations, tree refresh | Electron file API | File tree, open file | | `useExperimentHandlers` | `use-experiment-handlers.ts` | A/B test scanning, PR creation | experimentScanner, GitHub API | Experiment list, scan status | | `useKeafabGenerator` | `use-keafab-generator.ts` | Variant generation orchestration | keafabApi, telemetryService | isGenerating, progress, variants | | `useSimpleVariantApplier` | `use-variant-applier.ts` | Preview/apply interface | variantApplier | previewState, appliedVariants | | `useCROActions` | `use-cro-actions.ts` | Chat drawer action handlers | generator, applier, telemetry | Combined state | | `use-cro-state` | `use-cro-state.ts` | Component-level transient state | chatPersistence (auto-save) | All chat UI state | | `use-prompt-suggestions` | `use-prompt-suggestions.ts` | AI prompt suggestions | keafabApi.suggest() | suggestions, isLoading | | `use-cro-telemetry` | `use-cro-telemetry.ts` | Session and chat tracking | telemetryService | sessionId, chatId | | `use-conversation-history` | `use-conversation-history.ts` | Multi-element history | chatPersistence | conversationHistory Map | | `use-code-repair` | `use-code-repair.ts` | Error detection & auto-repair | keafabCodeRepair, autoIssueReporter | isRepairing, hasBuildError, error patterns | ### Hook Composition Pattern **Example**: `useCROActions` composes multiple hooks ```typescript function useCROActions(selectedElement, sourceCode) { // Compose specialized hooks const generator = useKeafabGenerator(); const applier = useSimpleVariantApplier(selectedElement); const telemetry = useCROTelemetry(); // Orchestrate workflow const handleGenerate = async (goal: string) => { // 1. Track start await telemetry.trackGenerationStart(goal); // 2. Generate const result = await generator.generate(goal, { selectedComponent: selectedElement, sourceCode, }); // 3. Auto-preview await applier.preview(result.variant.code); // 4. Track complete await telemetry.trackGenerationComplete(result); return result; }; return { handleGenerate, handlePreview, handleApply }; } ``` This pattern **separates concerns** while providing a unified API for UI components. *** ## Integration Architecture ### System Diagram ```mermaid sequenceDiagram participant User participant IDE as Keak-Code IDE participant DevServer as Dev Server (localhost) participant Dashboard as Dashboard API participant KeaFab as KeaFab API participant TelemetryDB as Telemetry DB User->>IDE: Opens project folder IDE->>DevServer: Start dev server (npm run dev) DevServer-->>IDE: WebSocket connection established User->>DevServer: Click element in preview DevServer->>IDE: WebSocket: element data IDE->>IDE: Resolve element → source file IDE->>IDE: Inject keakId if missing IDE->>Dashboard: POST /telemetry/chats/start Dashboard->>TelemetryDB: Record chat User->>IDE: Enter prompt IDE->>Dashboard: POST /keafab/generate/stream (with auth token) Dashboard->>KeaFab: POST /cro/generate/stream (with API key) KeaFab-->>Dashboard: SSE progress stream Dashboard-->>IDE: SSE progress stream IDE->>Dashboard: POST /telemetry/generations/start KeaFab-->>Dashboard: Complete variant Dashboard-->>IDE: Complete variant IDE->>Dashboard: POST /telemetry/generations/complete User->>IDE: Preview variant IDE->>IDE: FileStateManager.applyPreview() IDE->>DevServer: File written (HMR reload) IDE->>Dashboard: POST /telemetry/variants/preview User->>IDE: Apply variant IDE->>IDE: Wrap in component IDE->>Dashboard: POST /telemetry/variants/accept ``` ### Integration Points #### 1. Dashboard API Integration **Authentication** (`state/auth.ts`): ```typescript interface AuthState { isAuthenticated: boolean; user: { email: string; id: string } | null; token: string | null; } ``` **Project Management** (`state/desktop-project.ts`): ```typescript interface DesktopProjectState { currentProject: { project_uuid: string; domain_id: string | null; project_name: string; } | null; } ``` **API Endpoints Used**: - `POST /auth/login` - JWT token - `GET /projects/:id` - Project details - `POST /keafab/generate/stream` - Variant generation (proxy to KeaFab) - `POST /keafab/suggest` - Prompt suggestions (proxy to KeaFab) - `POST /telemetry/*` - Event tracking #### 2. KeaFab API (via Dashboard Proxy) **Why Proxy?** - **Security**: No API keys in Electron app - **Control**: Update KeaFab without IDE releases - **Centralization**: Single point for LLM access - **Authentication**: Dashboard validates user tokens **Request Flow**: ```typescript // IDE → Dashboard await fetch(`${dashboardUrl}/keafab/generate/stream`, { method: 'POST', headers: { 'Authorization': `Bearer ${userToken}` }, body: JSON.stringify({ full_page, selected_component, user_prompt, screenshot_url }) }); // Dashboard → KeaFab await fetch(`${keafabUrl}/cro/generate/stream`, { method: 'POST', headers: { 'Authorization': `Bearer ${keafabApiKey}` }, body: JSON.stringify(request) }); ``` The IDE never sees the KeaFab API key. #### 3. Telemetry Database **Separate Supabase instance** from Dashboard database. **Tracked Events**: - Sessions (work periods) - Chats (element conversations) - Generations (AI requests with tokens, duration) - Variants (code, edits, outcome, feedback) - Preview events - Acceptance/rejection See [Telemetry](./telemetry) for details. #### 4. Dev Server Integration **WebSocket Communication**: ```typescript // Preview browser → Dev server window.postMessage({ type: 'element-selected', data: { outerHTML: element.outerHTML, tagName: element.tagName, className: element.className, textContent: element.textContent, attributes: { 'data-keak-src': '...', 'data-keak-id': '...' } } }, '*'); // Dev server → IDE ws.send(JSON.stringify({ type: 'element-selected', data: elementData })); ``` **HMR (Hot Module Replacement)**: - File writes trigger automatic reload - Preview updates instantly after swap - No manual refresh needed *** ## State Management Architecture Keak-Code uses **Zustand stores** for global state with **localStorage persistence**. ### Zustand Stores | Store | File | Purpose | Persisted? | |-------|------|---------|-----------| | `useAuth` | `state/auth.ts` | Authentication state | ✅ (token, user) | | `useProject` | `state/project.ts` | Project files, dev server, experiments | ❌ (runtime only) | | `useDesktopProject` | `state/desktop-project.ts` | Dashboard project linking | ✅ (project UUID) | | `useKeakStore` | `state/chatStore.ts` | Chat tabs, element registry | ✅ (partial: elements, chats, openTabs) | | `useGitHub` | `state/github.ts` | OAuth flow, repo import | ❌ (runtime only) | | `useEventsStore` | `state/events.ts` | Analytics events | ❌ (scanned on load) | ### useProject Store (Example) ```typescript interface ProjectState { // Project root: string | null; tree: FileTreeNode | null; openFile: string | null; code: string; // Dev Server devServerUrl: string | null; devServerRunning: boolean; devServerPort: number | null; // Experiments experiments: Experiment[]; // Actions setRoot: (root: string) => void; setOpenFile: (file: string, code: string) => void; setDevServerUrl: (url: string) => void; // ... } ``` ### State Flow ``` User Action ↓ Hook (orchestrates) ↓ Service (executes) ↓ Zustand Store (updates) ↓ React Component (re-renders) ``` See [State Management](./state-management) for complete 3-tier architecture. *** ## Key Design Decisions ### 1. Why Three-Panel Layout? **Design Goals**: - Familiar file tree (left) - Focus on single task (center) - Context-aware tools (right) **Alternatives Considered**: | Alternative | Why Rejected | |-------------|--------------| | **Single panel** | No file tree, no simultaneous code + preview | | **Two panels** | No space for chat + experiments + events | | **Four+ panels** | Too complex, cognitive overload | Three panels provide **optimal balance** of functionality and focus. ### 2. Why No Routing? **State-based conditional rendering** instead of React Router. ```typescript // No routing - just conditionals if (!isAuthenticated) return ; if (!root) return ; return ; ``` **Why?** - **Simpler** - No route configuration, no URL parsing - **Faster** - Direct component mounting, no route matching - **Desktop-appropriate** - Electron apps don't need URLs - **Less code** - No Router, Routes, useNavigate, etc. **Trade-off**: No deep linking, but not needed for desktop app. ### 3. Why Monaco Instead of Full IDE? **Keak-Code is not VS Code.** Code editing is secondary; primary workflow is **visual**. **Monaco's Role**: - **Read-only viewing** with syntax highlighting - **Element highlighting** when selected from preview - **Jump to code** from variant cards **For complex edits**: Users open in VS Code (external editor integration). **Why Not Full Editor?** - **Scope creep** - Building a full IDE is not the goal - **Maintenance burden** - Monaco is industry-standard - **Performance** - Lightweight, fast startup - **Familiarity** - Same editor as VS Code ### 4. Why Zustand Over Redux? | Zustand | Redux | |---------|-------| | `create(set => ({...}))` | Actions, reducers, dispatch, connect | | Built-in persistence middleware | Need redux-persist | | ~1KB | ~10KB (with toolkit) | | Direct TypeScript inference | Manual typing needed | | Less boilerplate | More boilerplate | **Zustand provides**: - **Simpler API** (less code to maintain) - **Better DX** (less context switching) - **Smaller bundle** (faster load times) **Redux advantages** (not needed here): - Time-travel debugging (overkill for this app) - DevTools (Zustand has devtools too) - Middleware ecosystem (not needed) ### 5. Why Dashboard Proxy for KeaFab? **The IDE never calls KeaFab directly.** All AI generation goes through Dashboard. ``` IDE ──(auth token)──> Dashboard ──(API key)──> KeaFab ``` **Advantages**: | Concern | Solution | |---------|----------| | **Security** | API keys never in Electron app (would be extractable) | | **Updates** | Change KeaFab endpoint without IDE release | | **Centralization** | Single auth point, quota management, billing | | **Flexibility** | Switch LLM providers without IDE changes | **Trade-off**: Extra network hop (Dashboard → KeaFab), but negligible latency (~5-10ms). **Implementation** (`services/keafabApi.ts`): ```typescript export async function generateVariantStreaming( request: GenerateRequest, onProgress: (phase: string, message: string) => void ): Promise { const dashboardUrl = getConfig().backend_url; const token = useAuth.getState().token; const response = await fetch(`${dashboardUrl}/keafab/generate/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` // User token, not API key }, body: JSON.stringify(request) }); // Parse SSE stream const reader = response.body.getReader(); // ... stream parsing } ``` *** ## File Organization ### Directory Structure ``` keak-code/src/renderer/src/ │ ├── components/ # React components │ ├── CRODrawer.tsx # Main variant generation UI │ ├── PreviewPanel.tsx # Embedded browser │ ├── CodeEditor.tsx # Monaco wrapper │ ├── FileTree.tsx # Project file explorer │ ├── ExperimentPanel.tsx # A/B test management │ ├── CRODrawer/ # CRODrawer subcomponents │ │ ├── ChatTabBar.tsx │ │ ├── VariantsList.tsx │ │ ├── SuggestedPrompts.tsx │ │ ├── HistoryPanel.tsx │ │ └── ... │ └── ui/ # Shared UI components │ ├── hooks/ # Custom React hooks │ ├── use-cro-actions.ts │ ├── use-cro-state.ts │ ├── use-keafab-generator.ts │ ├── use-variant-applier.ts │ ├── use-prompt-suggestions.ts │ └── ... │ ├── services/ # Pure TypeScript services │ ├── variantApplier.ts │ ├── fileStateManager.ts │ ├── chatPersistence.ts │ ├── keafabApi.ts │ ├── telemetryService.ts │ ├── projectSetup.ts │ └── ... │ ├── state/ # Zustand stores │ ├── auth.ts │ ├── project.ts │ ├── chatStore.ts │ ├── desktop-project.ts │ └── ... │ ├── utils/ # Utility functions │ ├── elementBoundaries.ts │ ├── elementId.ts │ ├── wrapFlag.ts │ └── ... │ ├── config/ # Configuration │ └── auth.ts │ ├── App.tsx # Main entry point └── main.tsx # React mount point ``` ### Naming Conventions - **Components**: PascalCase, `.tsx` extension - **Hooks**: camelCase with `use` prefix - **Services**: camelCase, `.ts` extension - **Stores**: camelCase with `use` prefix - **Utils**: camelCase, `.ts` extension *** ## Data Flow Architecture ### Complete Flow: Element Selection → Variant Applied ``` 1. User clicks element in preview ↓ WebSocket 2. App.tsx receives element data ↓ useElementSourceResolver hook 3. Resolve source file (Babel metadata or keakId search) ↓ 4. Check if keakId exists in source ↓ (if missing) utils/elementId.ts 5. Inject data-keak-id attribute ↓ FileStateManager 6. Register element, restore snapshot if exists ↓ chatPersistence 7. Load conversation history for keakId ↓ CRODrawer hydrates 8. Display chat UI, open tab ↓ 9. User enters prompt ↓ useCROActions.handleGenerate 10. Call useKeafabGenerator.generate ↓ services/keafabApi.ts 11. POST to Dashboard /keafab/generate/stream ↓ Dashboard proxies to KeaFab 12. Stream progress updates (router → optimizer → validator) ↓ services/telemetryService.ts 13. Track generation start + complete ↓ useSimpleVariantApplier.preview 14. Call variantApplier.previewVariant ↓ fileStateManager.applyPreview 15. Read disk, capture control, write variant ↓ Electron API 16. Write file to disk ↓ Dev server HMR 17. Preview reloads, shows variant ↓ 18. User clicks "Apply Variant" ↓ useCROActions.handleApply 19. Wrap in component (utils/wrapFlag.ts) ↓ services/telemetryService.ts 20. Track variant acceptance ↓ 21. Done - experiment live in code ``` This flow touches **every layer** of the architecture: - UI (CRODrawer) - Hooks (useCROActions, useKeafabGenerator, useSimpleVariantApplier) - Services (keafabApi, variantApplier, fileStateManager, telemetryService) - State (Zustand stores, chatPersistence) - External (Dashboard API, KeaFab API, Telemetry DB) *** ## Performance Considerations ### Debounced Operations **ChatPersistence Auto-Save** (500ms): ```typescript useEffect(() => { const timeoutId = setTimeout(() => { chatPersistence.saveChat(keakId, state); }, 500); return () => clearTimeout(timeoutId); }, [state]); ``` **Benefit**: 10 rapid changes → 1 disk write ### Cached Boundaries ```typescript interface ElementSnapshot { boundaries?: ElementBoundaries; // Cached after first lookup } ``` **Benefit**: Subsequent swaps skip boundary detection (~50ms saved) ### In-Memory File Content ```typescript class FileStateManager { private currentContent = new Map(); // Cache } ``` **Benefit**: Avoid redundant file reads during rapid swaps ### Smart Validation Skipping KeaFab Validator is called only when heuristics detect potential issues (malformed HTML, missing keakId, etc.). Clean variants skip validation entirely. *** ## Security Architecture ### No API Keys in Electron **Problem**: Electron apps can be decompiled → API keys extractable. **Solution**: All API keys (KeaFab, Supabase, etc.) live in Dashboard backend. ### Token Management ```typescript // IDE stores JWT token (localStorage) const token = useAuth.getState().token; // Sent with every Dashboard request headers: { 'Authorization': `Bearer ${token}` } // Dashboard validates token + user permissions // Then calls upstream services with API keys ``` **Token Expiration**: 7 days, auto-refresh on window focus. ### File System Access Electron's sandbox limits file access to: - Project root directory - `.keak` directory - Temp directory for screenshots No access to system files or other user data. *** ## Extensibility ### Adding a New Framework **Steps**: 1. Add detection logic to `projectSetup.ts`: ```typescript function detectFramework(root: string): Framework { if (hasFile('astro.config.js')) return 'astro'; // ... } ``` 2. Add setup handler: ```typescript async function setupAstro(root: string) { await installSDK(root); await injectBabelPlugin(root, 'astro'); await configureDevServer(root, 'astro'); } ``` 3. Add dev server config: ```typescript const devServerCommands = { astro: 'npm run dev -- --port {port}' }; ``` **No changes needed** in: - Element selection (works with any DOM) - Variant generation (framework-agnostic JSX) - Preview system (swaps source code) ### Adding a New Service **Steps**: 1. Create service file in `services/`: ```typescript // services/myService.ts export async function myOperation(params) { // Pure logic, no React } ``` 2. Create hook wrapper: ```typescript // hooks/use-my-service.ts export function useMyService() { return useMemo(() => ({ doSomething: () => myOperation(...) }), []); } ``` 3. Use in component: ```typescript const myService = useMyService(); await myService.doSomething(); ``` **Pattern**: Services are pure logic, hooks provide React integration. *** ## Critical Files Reference | File | Lines | Purpose | Dependencies | |------|-------|---------|--------------| | `App.tsx` | ~2400 | Main application entry, layout orchestration | All hooks, all components | | `CRODrawer.tsx` | ~800 | Variant generation UI | useCROActions, useKeafabGenerator, useSimpleVariantApplier | | `variantApplier.ts` | ~200 | Preview/apply operations | fileStateManager, elementBoundaries | | `fileStateManager.ts` | ~300 | Core swap logic | elementBoundaries, electronAPI | | `chatPersistence.ts` | ~150 | History storage | electronAPI | | `keafabApi.ts` | ~250 | AI generation client | useAuth (for token) | | `elementBoundaries.ts` | ~400 | JSX parser | None (pure algorithm) | | `use-cro-state.ts` | ~500 | Component state management | chatPersistence (auto-save) | *** ## Related Documentation - **[Core Concepts](./core-concepts)** - Mental models for understanding the architecture - **[Preview System](./preview-system)** - FileStateManager implementation details - **[State Management](./state-management)** - Complete 3-tier architecture - **[Element Tracking](./element-tracking)** - Selection flow and WebSocket protocol --- /docs/extension-ide/ide/babel-integration # Babel Integration **The Foundation**: How element tagging during build enables instant source resolution. Without the Babel plugin, element selection would require **slow file searches** (~100-500ms). With it, source files are resolved **instantly** (~1-5ms). *** ## Purpose of Babel Plugin ### The Problem When a user clicks an element in the preview browser, we need to answer: - **Which file** defines this element? - **Which line** is the element on? - **Which column** does it start at? **Without source mapping**: ``` User clicks ``` ### After Babel Plugin ```jsx ``` ### Attribute Meanings | Attribute | Value | Purpose | |-----------|-------|---------| | `data-keak-src` | `"components/hero.tsx:42:8"` | Source location (file:line:column) | | `data-keak-component` | `"HeroSection"` | React component name (for context) | | `data-keak-idx` | `"0"` | Element index within component (0-based) | **Note**: `data-keak-id` is **NOT** injected by Babel. It's injected on-demand by the IDE when an element is selected (see [Element Tracking](./element-tracking.md#step-4-keakid-generation--injection)). *** ## Plugin Installation ### Automatic Setup **When**: User opens a project in Keak-Code IDE. **What happens**: `projectSetup.ts` detects framework and configures Babel plugin. **Detection logic**: ```typescript async function detectFramework(projectRoot: string): Promise { const files = await listFiles(projectRoot); // Check for framework indicators if (files.includes('next.config.js')) { return files.includes('app') ? 'nextjs-app' : 'nextjs-pages'; } if (files.includes('vite.config.ts')) { return 'vite'; } if (files.includes('react-scripts')) { return 'create-react-app'; } if (files.includes('index.html')) { return 'static-html'; } return 'unknown'; } ``` ### Plugin Configuration **Injected config** (auto-generated): ```javascript // babel.config.js module.exports = { presets: [ // ... existing presets ], plugins: [ // ... existing plugins ['@keak/babel-plugin-keak', { enabled: process.env.NODE_ENV === 'development', // Dev only verbose: true // Show logs }] ] }; ``` **Key options**: - `enabled` - Only run in development (not production) - `verbose` - Log processing details **Why development only?** - **Production**: Source maps not needed - **Security**: Don't expose file paths publicly - **Size**: Saves ~100 bytes per element (minor, but unnecessary) ### Framework-Specific Setup #### Next.js ```javascript // next.config.js module.exports = { webpack: (config) => { // Babel plugin is auto-configured via .babelrc return config; } }; // .babelrc { "presets": ["next/babel"], "plugins": [ ["@keak/babel-plugin-keak", { "enabled": true }] ] } ``` #### Vite ```javascript // vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [ react({ babel: { plugins: [ ['@keak/babel-plugin-keak', { enabled: true }] ] } }) ] }); ``` #### Create React App ```javascript // babel.config.js (create if doesn't exist) module.exports = { presets: ['react-app'], plugins: [ ['@keak/babel-plugin-keak', { enabled: process.env.NODE_ENV === 'development' }] ] }; ``` *** ## Build Process ### Compilation Flow ``` 1. Dev server starts (npm run dev) 2. Webpack/Vite processes files 3. Babel transforms JSX 4. Keak plugin intercepts JSX elements 5. Plugin injects attributes 6. Compilation continues 7. Dev server serves app with attributes ``` **Build logs** show plugin activity: ``` [Keak Babel Plugin] Starting... [Keak Babel Plugin] Processing src/components/hero.tsx... [Keak Babel Plugin] Tagged 8 elements in hero.tsx [Keak Babel Plugin] Processing src/components/pricing.tsx... [Keak Babel Plugin] Tagged 12 elements in pricing.tsx [Keak Babel Plugin] Processing src/components/footer.tsx... [Keak Babel Plugin] Tagged 5 elements in footer.tsx [Keak Babel Plugin] ✓ Tagged 157 elements across 24 files in 342ms ``` **Performance**: ~10-20ms per file (negligible impact on build time). ### What Gets Tagged **Tagged** (JSX elements): - `"` - HTML in comments: `{/* ` | `cta-primary-button` | | `
No text
` | `element-a3f2e8-div` | ### Injection Process **Triggered**: When element selected without `data-keak-id` in source. **Implementation**: ```typescript async function injectDataIdIntoSource( filePath: string, keakId: string, elementCode: string ): Promise { const fileContent = await readFile(filePath); const lines = fileContent.split('\n'); // Find element opening tag const { startLine, startColumn } = findElementStart(lines, elementCode); // Find end of opening tag (handles multi-line) const tagEndIndex = findTagEnd(lines, startLine, startColumn); const newAttribute = ` data-keak-id="${keakId}"`; // Insert at optimal location (after tag name, before other attrs) lines[startLine] = insertAfterTagName(lines[startLine], newAttribute); await writeFile(filePath, lines.join('\n')); } ``` **Result**: ```jsx // Before // After ``` ### Self-Healing If a keakId is accidentally removed (e.g., code formatting tool strips it), the system **automatically re-injects** it on next element selection. **Detection**: ```typescript const hasKeakId = await searchForKeakIdInFile(filePath, keakId); if (!hasKeakId) { await injectDataIdIntoSource(filePath, keakId, elementCode); } ``` This ensures conversation history remains linked to the element even after destructive edits. *** ## Control vs Variant (The Just-in-Time Swap) ### Core Principle **Two versions, one disk slot.** ``` ┌──────────────────┐ │ File on Disk │ │ (Single Truth) │ └────────┬─────────┘ │ ┌──────────┴──────────┐ │ │ activePreview = false activePreview = true │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ CONTROL │ │ VARIANT │ │ (original) │ │ (generated) │ └─────────────┘ └─────────────┘ ▲ ▲ │ │ └──────────┬───────────┘ │ Swap captures edits from disk FIRST ``` ### The Swap State Machine ``` Preview: Control → Read disk (capture control edits) → Write variant → activePreview=true Hide: Variant → Read disk (capture variant edits) → Write control → activePreview=false ``` **Key insight**: The system **always reads from disk first** before swapping. This captures any edits made to the currently active version (whether in the IDE, VS Code, or any editor). ### Why This Design? | Property | Benefit | |----------|---------| | **Lossless** | Both versions capture all edits, nothing lost | | **No file watchers** | State captured at action time only, simpler and more reliable | | **Offline-safe** | `activePreview` bookmark survives app restarts | | **Editor-agnostic** | Works with external editors (VS Code, Vim, etc.) | | **Stateless swapping** | Only *which version was active* matters, not *when* edits occurred | ### Example: User Edits Control **Initial state**: ``` Disk: Snapshot: { originalCode: "", variantCode: null, activePreview: false } ``` **User edits control** (in VS Code while IDE closed): ``` Disk: ``` **User clicks "Preview"**: ```typescript // 1. Read disk (captures control edits!) const currentCode = readDisk(); // "" // 2. Capture to control snapshot.originalCode = currentCode; // 3. Write variant writeDisk(variantCode); // 4. Update flag snapshot.activePreview = true; ``` **Result**: ``` Disk: // Variant Snapshot: { originalCode: "", // ✅ Edits preserved! variantCode: "", activePreview: true } ``` The control edits are **safely captured** even though they were made while the IDE was closed. *** ## Element Source Resolution ### Two-Stage Resolution Keak-Code uses a **fallback chain** to locate element source files: #### Stage 1: Babel Plugin Metadata (Primary) The Babel plugin injects `data-keak-src` during build: ```html ", "tagName": "button", "textContent": "Sign Up", "className": "cta-primary" }, "sourceFilePath": "components/hero.tsx", "messages": [ { "role": "user", "content": "Make it more compelling" }, { "role": "assistant", "content": "Analysis...", "variant": {...} } ], "variants": [ { "id": "v1", "code": "", ... } ], "elementSnapshot": { "controlCode": "", "variantCode": "", "activePreview": true }, "previewVariantId": "v1", "timestamp": 1234567890 }, "pricing-cta-button": { // Different element, different conversation } } } ``` ### Conversation Lifecycle ``` Element Selected ↓ Check if keakId exists in .keak/history/history.json ↓ ┌─────────────┬─────────────┐ │ Exists │ Not Exists │ │ (resume) │ (new) │ └──────┬──────┴──────┬──────┘ ↓ ↓ Load history Initialize fresh │ │ └──────┬──────┘ ↓ Hydrate UI state ↓ User generates variants ↓ Auto-save to history (500ms debounce) ↓ Close drawer (state persisted) ↓ Reopen element → Full state restored ``` ### Multi-Element Conversations Users can have **multiple chat tabs open simultaneously**: ```typescript // CRODrawer state const [openChats, setOpenChats] = useState([ { keakId: 'hero-cta-button', elementInfo: {...} }, { keakId: 'pricing-table', elementInfo: {...} }, { keakId: 'footer-link', elementInfo: {...} } ]); const [activeKeakId, setActiveKeakId] = useState('hero-cta-button'); ``` Each tab maintains independent: - Message history - Generated variants - Preview state - FileStateManager snapshot Switching tabs triggers hydration from ChatPersistence for the newly active element. *** ## Snapshot System (Preventing Data Corruption) ### The Problem **Without snapshots**, this bug occurs: ``` 1. User previews variant (writes variant to disk) 2. App crashes 3. User reopens IDE and selects same element 4. FileStateManager.registerElement() reads disk 5. Captures variant as control ❌ 6. Original control code is lost forever ``` ### The Solution Persist `elementSnapshot` with chat data: ```typescript interface ElementSnapshot { controlCode: string; // Original element code variantCode: string | null; // Generated variant code activePreview: boolean; // Which version is on disk } ``` The `activePreview` flag acts as a **bookmark** that tells FileStateManager which version is currently on disk. ### Restoration Flow **Critical**: Snapshot restoration happens **BEFORE** element registration. ```typescript // Step 1: Load persisted chat const persistedChat = await ChatPersistence.loadChat(keakId); // Step 2: Restore snapshot FIRST (if exists) if (persistedChat?.elementSnapshot) { FileStateManager.restoreSnapshot( keakId, filePath, persistedChat.elementSnapshot ); } // Step 3: NOW safe to register element FileStateManager.registerElement(keakId, filePath, fileContent); ``` **How it works**: ```typescript function restoreSnapshot( keakId: string, filePath: string, snapshot: { controlCode, variantCode, activePreview } ): void { // Inject pre-existing snapshot into FileStateManager this.elementSnapshots.set(keakId, { keakId, filePath, originalCode: snapshot.controlCode, variantCode: snapshot.variantCode, activePreview: snapshot.activePreview }); } function registerElement(keakId: string, filePath: string, fileContent: string): void { const snapshot = this.elementSnapshots.get(keakId); const boundaries = findElementBoundaries(fileContent, keakId); const currentCode = extractElementCode(fileContent, boundaries); if (snapshot) { // Snapshot already exists (restored from persistence) if (snapshot.activePreview) { // Variant is on disk → update variant, preserve control snapshot.variantCode = currentCode; // snapshot.originalCode stays unchanged ✅ } else { // Control is on disk → update control, preserve variant snapshot.originalCode = currentCode; // snapshot.variantCode stays unchanged ✅ } } else { // Fresh element (no snapshot) this.elementSnapshots.set(keakId, { keakId, filePath, originalCode: currentCode, variantCode: null, activePreview: false }); } } ``` ### Why This Works The `activePreview` flag tells `registerElement()` the **truth** about which version is on disk, so it updates the correct slot: - `activePreview=true` → Update `variantCode`, preserve `originalCode` - `activePreview=false` → Update `originalCode`, preserve `variantCode` This prevents data corruption on app restarts. ### Snapshot Persistence Snapshots are **automatically included** in ChatPersistence auto-save: ```typescript // use-cro-state.ts auto-save effect useEffect(() => { const timeoutId = setTimeout(() => { // Get latest snapshot from FileStateManager const snapshot = getFileStateManager().getSnapshot(keakId); // Include in persistence await ChatPersistence.saveChat(keakId, { ...chatState, elementSnapshot: snapshot ? { controlCode: snapshot.originalCode, variantCode: snapshot.variantCode, activePreview: snapshot.activePreview } : null }); }, 500); return () => clearTimeout(timeoutId); }, [/* dependencies */]); ``` This ensures snapshots are always in sync with conversation history. *** ## FileStateManager (The Swap Engine) The FileStateManager is the core service that enables lossless preview/hide operations. ### Data Structures **Location**: `services/fileStateManager.ts` ```typescript class FileStateManager { // Map: keakId → element snapshot private elementSnapshots = new Map(); // Map: filePath → in-memory file content (cache) private currentContent = new Map(); } interface ElementSnapshot { keakId: string; filePath: string; originalCode: string; // Control version variantCode: string | null; // Treatment version activePreview: boolean; // true = variant on disk, false = control on disk boundaries?: ElementBoundaries; // Cached for performance } ``` ### Key Methods ```typescript class FileStateManager { // Register element (initial capture) registerElement(keakId: string, filePath: string, fileContent: string): void // Preview variant (control → variant) applyPreview(filePath: string, keakId: string, fileContent: string, variantCode: string): { newContent: string; boundaries: ElementBoundaries; } // Hide preview (variant → control) hidePreview(filePath: string, keakId: string, fileContent: string): { newContent: string; capturedVariantCode: string; } // Get both codes for experiment wrapping getCodesForExperiment(filePath: string, keakId: string, fileContent: string): { controlCode: string; variantCode: string; boundaries: ElementBoundaries; } // Restore from persistence restoreSnapshot(keakId: string, filePath: string, snapshot: ElementSnapshot): void // Get snapshot for persistence getSnapshot(keakId: string): ElementSnapshot | null } ``` ### Why FileStateManager? **Centralized swap logic** ensures: - Consistent behavior across all preview/hide operations - Single source of truth for element snapshots - No duplication of swap logic - Easy to debug (all swaps go through one place) **Alternative (rejected)**: Each component manages its own swaps → duplicated logic, inconsistent behavior, bugs. *** ## Persistence Strategy ### 3-Tier Architecture Keak-Code uses **three persistence layers** with different durability guarantees: ``` ┌─────────────────────────────────────────────────────────────┐ │ Tier 1: Zustand Store (Global UI State) │ │ Persistence: localStorage (partial) │ │ Survives: Tab refresh, but not localStorage clear │ │ Stores: elements, chats, openTabs │ └─────────────────────────────────────────────────────────────┘ ▼ Auto-sync ┌─────────────────────────────────────────────────────────────┐ │ Tier 2: ChatPersistence (Project History) │ │ Persistence: .keak/history/history.json │ │ Survives: App restarts, drawer closures, git commits │ │ Stores: Full conversation state + elementSnapshot │ └─────────────────────────────────────────────────────────────┘ ▲ 500ms debounce ┌─────────────────────────────────────────────────────────────┐ │ Tier 3: React Component State (Transient) │ │ Persistence: None (ephemeral) │ │ Survives: Only while component mounted │ │ Stores: Active session data (variants, messages, etc.) │ └─────────────────────────────────────────────────────────────┘ ``` ### Hydration Priority When loading state for an element: ```typescript async function hydrateElement(keakId: string): Promise { // Priority 1: ChatPersistence (most durable) const persisted = await ChatPersistence.loadChat(keakId); if (persisted) return persisted; // Priority 2: In-memory conversationHistory (survives tab switch) const inMemory = conversationHistory.get(elementId); if (inMemory) return reconstructStateFromConversation(inMemory); // Priority 3: Fresh initialization (new chat) return createFreshChatState(); } ``` **Why this order?** - **ChatPersistence** survives everything (file-based) - **conversationHistory** is fast but lost on drawer close - **Fresh state** only for truly new conversations ### Auto-Save Strategy **Debounced writes** prevent excessive disk I/O: ```typescript // use-cro-state.ts useEffect(() => { const timeoutId = setTimeout(() => { ChatPersistence.saveChat(keakId, completeState); }, 500); // 500ms debounce return () => clearTimeout(timeoutId); }, [messages, variants, userGoal, /* ...all dependencies */]); ``` **Behavior**: - User makes 10 rapid changes → Only 1 write (500ms after last change) - User switches tabs → Save triggered immediately (cleanup function) - App crashes → Last saved state recovered (max 500ms data loss) *** ## Element Boundaries ### The Challenge Given a keakId, **precisely extract** the element's code from source lines: ```jsx // File content: const Hero = () => (

Create stunning web experiences

); ``` **Goal**: Extract just the `

...

` element (lines 2-4) when given `"hero-headline"`. ### The Algorithm (Stack-Based JSX Parser) **Location**: `utils/elementBoundaries.ts` **Strategy**: Use a **stack** to match opening and closing tags, handling arbitrary nesting depth. **Steps**: 1. **Find keakId location** (line + column) 2. **Extract tag name** (`` → return immediately) 4. **Stack-based search** for matching closing tag - Push opening tags onto stack - Pop closing tags from stack - Stack empty → found our closing tag **Edge cases handled**: - Multi-line opening tags - Nested elements of same type (`
`) - String literals with angle brackets (`const x = "a < b"`) - JSX expressions with tags (`{value && ...}`) See [Element Selection & Tracking](./element-tracking) for full implementation. *** ## Related Concepts - **[Preview System](./preview-system)** - How control ↔ variant swapping works in detail - **[State Management](./state-management)** - Complete 3-tier architecture and data flows - **[Element Selection & Tracking](./element-tracking)** - Boundary detection algorithm and source resolution - **[Architecture](./architecture)** - How these concepts fit into the overall system --- /docs/extension-ide/ide/element-tracking # Element Selection & Tracking **The Journey**: How a user's click in the preview browser transforms into a conversation about code in the source file. *** ## The Story: From Click to Code When you click an element in Keak-Code's preview, a chain of events unfolds: 1. **Browser captures** the element's data (HTML, classes, text) 2. **WebSocket sends** the data to the IDE 3. **IDE resolves** which source file contains this element 4. **System ensures** the element has a stable identifier (keakId) 5. **FileStateManager registers** the element for tracking 6. **ChatPersistence loads** any previous conversation about this element 7. **CRO Drawer opens**, ready for you to generate variants This document explains each step in detail. *** ## Step 1: Element Selection in Preview ### User Clicks Element The preview browser has the **Keak Toolbar** injected into the iframe. When you click an element: ```typescript // Injected toolbar script element.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // Capture element data const elementData = { outerHTML: element.outerHTML, innerHTML: element.innerHTML, tagName: element.tagName.toLowerCase(), className: element.className, textContent: element.textContent?.trim() || '', attributes: extractAllAttributes(element) }; // Send to dev server via postMessage window.postMessage({ type: 'element-selected', data: elementData }, '*'); }); ``` **Captured Data**: ```typescript { outerHTML: "", innerHTML: "Sign Up", tagName: "button", className: "cta-primary", textContent: "Sign Up", attributes: { "class": "cta-primary", "data-keak-src": "components/hero.tsx:42:8", // From Babel plugin! "data-keak-component": "HeroSection", "data-keak-idx": "0" } } ``` Notice `data-keak-src` - this is **gold**. The Babel plugin injected it during build, giving us the exact source location. *** ## Step 2: WebSocket Communication ### Message Flow ```mermaid sequenceDiagram participant User participant Browser as Preview Browser (iframe) participant DevServer as Dev Server (localhost:3000) participant IDE as Keak-Code IDE participant FS as File System User->>Browser: Clicks ``` **Parsing**: ```typescript function parseKeakSrc(keakSrc: string): { filePath: string; line: number; column: number; } { const [filePath, line, column] = keakSrc.split(':'); return { filePath, line: parseInt(line), column: parseInt(column) }; } ``` **Advantages**: - ✅ **Instant resolution** - No file search needed - ✅ **Line/column precision** - Can highlight exact location in Monaco - ✅ **Always accurate** - Direct from build process **Failure cases**: - File was moved after build - Babel plugin not configured - Attribute stripped by minifier #### Stage 2: KeakId Search (Fallback) If `data-keak-src` is missing or invalid, search for the `data-keak-id` attribute: ```typescript async function findElementByKeakId( keakId: string, projectRoot: string ): Promise { // Search all source files const files = await glob(`${projectRoot}/src/**/*.{tsx,jsx,ts,js}`); for (const file of files) { const content = await electronAPI.readFile(file); if (content.includes(`data-keak-id="${keakId}"`)) { return file; } } return null; // Not found } ``` **Advantages**: - ✅ **Survives file moves** - As long as attribute is in the file - ✅ **Robust to build changes** - Doesn't rely on Babel plugin **Trade-offs**: - ⏱️ **Slower** - Must search all files (~100-500ms for large projects) - ❌ **No line/column** - Must parse file to find element #### Stage 3: Manual Selection (Last Resort) If both fail, ask the user: ```typescript const filePath = await electronAPI.showOpenDialog({ title: 'Select source file for this element', defaultPath: projectRoot, filters: [ { name: 'Source Files', extensions: ['tsx', 'jsx', 'ts', 'js'] } ] }); ``` **When this happens**: - keakId was removed from source - Element is in a non-standard location - Unusual project structure ### Complete Resolution Function ```typescript async function resolveElementSource( elementData: ElementData, projectRoot: string ): Promise<{ filePath: string; line?: number; column?: number }> { // Priority 1: Babel plugin metadata const keakSrc = elementData.attributes['data-keak-src']; if (keakSrc) { const { filePath, line, column } = parseKeakSrc(keakSrc); const fullPath = path.join(projectRoot, filePath); if (await fileExists(fullPath)) { return { filePath: fullPath, line, column }; } } // Priority 2: KeakId search const keakId = elementData.attributes['data-keak-id'] || generateIdFromElementInfo(elementData); const filePath = await findElementByKeakId(keakId, projectRoot); if (filePath) { return { filePath }; } // Priority 3: Manual selection const manualPath = await showFileSelectionDialog(); return { filePath: manualPath }; } ``` *** ## Step 4: KeakId Generation & Injection ### Why KeakIds? **Problem**: We need a **stable identifier** that survives code changes. **Bad solutions**: - ❌ Line numbers (change when code above changes) - ❌ Content hashes (change when element text changes) - ❌ XPath selectors (brittle, change with DOM restructuring) **Good solution**: ✅ **Human-readable IDs based on element semantics** ### Generation Algorithm **Location**: `utils/elementId.ts` **Priority-based generation**: ```typescript export function generateIdFromElementInfo(info: ElementInfo): string { // Priority 1: Use textContent (most stable for content elements) if (info.textContent && info.textContent.trim()) { const sanitized = info.textContent .trim() .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') // Remove special chars .replace(/\s+/g, '-') // Spaces → hyphens .substring(0, 50); // Limit length return `${sanitized}-${info.tagName}`; // Example: "create-stunning-web-experiences-h1" } // Priority 2: Use className (stable for styled elements) if (info.className) { const firstClass = info.className.split(' ')[0]; return `${firstClass}-${info.tagName}`; // Example: "cta-primary-button" } // Priority 3: Hash-based fallback (when nothing else available) const hash = simpleHash(info.outerHTML); return `element-${hash}-${info.tagName}`; // Example: "element-a3f2e8-div" } ``` **Examples**: | Element | Generated KeakId | Why | |---------|------------------|-----| | `

Create stunning web experiences

` | `create-stunning-web-experiences-h1` | Text content (priority 1) | | `` | `sign-up-button` | Text content (priority 1) | | `
` | `hero-container-div` | Class name (priority 2, no text) | | `
Complex
` | `element-a3f2e8-div` | Hash (priority 3, nested text) | ### Injection Process **When**: Element selected but `data-keak-id` not found in source. **Why inject?**: Enables conversation persistence, self-healing, and grep-ability. **Implementation**: ```typescript async function injectDataIdIntoSource( filePath: string, keakId: string, elementCode: string ): Promise { try { // 1. Read source file const fileContent = await electronAPI.readFile(filePath); const lines = fileContent.split('\n'); // 2. Find element by matching code const { startLine, startColumn } = findElementInLines(lines, elementCode); // 3. Find end of opening tag const openingTag = extractFullOpeningTag(lines, startLine, startColumn); // 4. Inject attribute after tag name const updatedLine = injectAttributeAfterTagName( lines[startLine], startColumn, `data-keak-id="${keakId}"` ); lines[startLine] = updatedLine; // 5. Write back to disk await electronAPI.writeFile(filePath, lines.join('\n')); // 6. Update FileStateManager cache FileStateManager.updateCurrentContent(filePath, lines.join('\n')); return true; } catch (error) { console.error('[KeakId Injection] Failed:', error); return false; } } ``` **Result**: ```jsx // Before injection // After injection ``` The attribute is now **permanently in the source file**, surviving rebuilds and code formatting. ### Self-Healing **Scenario**: User's code formatter strips `data-keak-id` (rare, but possible). **Detection**: On next element selection, the IDE searches for the keakId in the source: ```typescript const hasKeakId = fileContent.includes(`data-keak-id="${keakId}"`); if (!hasKeakId) { console.warn('[Self-Healing] KeakId missing, re-injecting...'); await injectDataIdIntoSource(filePath, keakId, elementCode); } ``` **Result**: Conversation history remains linked to the element. The user never notices the attribute was removed. *** ## Step 5: Babel Plugin Integration ### What is the Babel Plugin? **A build-time transform** that automatically tags all JSX elements with source metadata during compilation. **Location**: Injected into `babel.config.js` during project setup **Purpose**: Enable **instant source resolution** without file searching. ### Injected Attributes **Before Babel plugin**: ```jsx ``` **After Babel plugin**: ```jsx ``` **Attribute meanings**: - `data-keak-src` - Source location (file:line:column) - `data-keak-component` - React component name - `data-keak-idx` - Element index within component (0-based) ### Plugin Configuration **Auto-injected during setup** (`projectSetup.ts`): ```javascript // babel.config.js module.exports = { plugins: [ ['@keak/babel-plugin-keak', { enabled: process.env.NODE_ENV === 'development', // Dev only verbose: true // Show logs }] ] }; ``` **Why development only?** - **Production**: Attributes add ~100 bytes per element (negligible, but unnecessary) - **Source maps**: Not needed in production - **Security**: Don't expose source file paths publicly ### Build Logs When dev server starts, you'll see: ``` [Keak Babel Plugin] Processing src/components/hero.tsx... [Keak Babel Plugin] Tagged 8 elements in hero.tsx [Keak Babel Plugin] Processing src/components/pricing.tsx... [Keak Babel Plugin] Tagged 12 elements in pricing.tsx [Keak Babel Plugin] ✓ Tagged 157 elements across 24 files in 342ms ``` This confirms the plugin is working. ### Webpack Loader Alternative **For projects without Babel** (e.g., pure Webpack setups): ```javascript // webpack.config.js module.exports = { module: { rules: [ { test: /\.(jsx|tsx)$/, use: [ '@keak/webpack-loader-pre', // Keak loader BEFORE babel-loader 'babel-loader' ] } ] } }; ``` **Build logs** will show `[keak-loader-pre]` instead of `[Keak Babel Plugin]`. **Same attributes injected**, different mechanism. *** ## Step 6: Element Boundary Detection ### The Challenge Given a keakId, **precisely extract the element's code** from source lines. **Example scenario**: ```jsx // File: components/hero.tsx const Hero = () => (

Create stunning web experiences

); ``` **Goal**: Extract just the `

...

` (lines 3-5) when given `"hero-headline"`. **Why it's hard**: - Multi-line opening tags - Nested elements of same type (`
`) - Self-closing tags (``) - String literals with angle brackets (`const x = "a < b"`) - JSX expressions with tags (`{condition && ...}`) ### The Algorithm: Stack-Based JSX Parser **Location**: `utils/elementBoundaries.ts` **Strategy**: Use a **stack** to track opening/closing tag pairs, handling arbitrary nesting depth. **Interface**: ```typescript interface ElementBoundaries { startLine: number; // 0-indexed startColumn: number; // 0-indexed (first character of '<') endLine: number; // 0-indexed endColumn: number; // 0-indexed (last character of '>') } function findElementBoundaries( sourceLines: string[], keakId: string ): ElementBoundaries ``` ### Algorithm Steps #### Step 1: Find Opening Tag ```typescript // Search for line containing data-keak-id="hero-headline" function findKeakIdLocation(sourceLines: string[], keakId: string): { line: number; column: number; } { for (let i = 0; i < sourceLines.length; i++) { const line = sourceLines[i]; const searchPattern = `data-keak-id="${keakId}"`; const index = line.indexOf(searchPattern); if (index !== -1) { // Find start of tag (search backwards for '<') const tagStart = line.lastIndexOf('<', index); return { line: i, column: tagStart }; } } throw new Error(`KeakId "${keakId}" not found in source`); } ``` #### Step 2: Extract Tag Name ```typescript const tagMatch = sourceLines[startLine] .substring(startColumn) .match(/<(\w+)/); if (!tagMatch) { throw new Error('Invalid JSX opening tag'); } const tagName = tagMatch[1]; // "h1", "button", "div", etc. ``` #### Step 3: Check if Self-Closing ```typescript const openingTag = extractFullOpeningTag(sourceLines, startLine, startColumn); if (openingTag.endsWith('/>')) { // Self-closing tag - we're done! return { startLine, startColumn, endLine: openingTag.lastLine, endColumn: openingTag.lastColumn + 2 // Include /> }; } ``` **Example**: `` → No closing tag needed. #### Step 4: Stack-Based Closing Tag Search **Core algorithm**: ```typescript const stack: string[] = [tagName]; // Start with our tag let currentLine = openingTag.lastLine; let currentColumn = openingTag.lastColumn; while (stack.length > 0 && currentLine < sourceLines.length) { const restOfLine = sourceLines[currentLine].substring(currentColumn); // Skip string literals (they might contain '<' or '>') const withoutStrings = removeStringLiterals(restOfLine); // Find all tags in this line const tagRegex = /<\/?(\w+)(\s|\/|>)/g; let match; while ((match = tagRegex.exec(withoutStrings)) !== null) { const isClosing = match[0].startsWith(''); const foundTag = match[1]; if (isSelfClosing) { // Self-closing like - ignore, doesn't affect stack continue; } if (isClosing) { // Closing tag like - pop from stack if (stack[stack.length - 1] === foundTag) { stack.pop(); if (stack.length === 0) { // Found our closing tag! return { startLine, startColumn, endLine: currentLine, endColumn: currentColumn + match.index + match[0].length }; } } } else { // Opening tag like
- push to stack stack.push(foundTag); } } // Move to next line currentLine++; currentColumn = 0; } throw new Error(`Could not find closing tag for <${tagName}>`); ``` **Visualization** (nested div example): ```jsx
// Stack: [div] - Push 'div'
// Stack: [div, div] - Push 'div' Inner //
// Stack: [div] - Pop 'div' Text // Stack: [div, span], then [div] - Push 'span', pop 'span'
// Stack: [] - Pop 'div' → DONE! ✅ ``` The stack ensures we match the **correct closing tag**, not the nested div's closing tag. ### Helper: Remove String Literals **Why?** String literals can contain angle brackets: ```jsx const message = "Value is < 10"; // This < should not be detected as a tag! ``` **Solution**: Replace string contents with spaces (preserves indices for regex matching): ```typescript function removeStringLiterals(code: string): string { return code .replace(/"(?:[^"\\]|\\.)*"/g, match => ' '.repeat(match.length)) .replace(/'(?:[^'\\]|\\.)*'/g, match => ' '.repeat(match.length)) .replace(/`(?:[^`\\]|\\.)*`/g, match => ' '.repeat(match.length)); } ``` **Before**: `const x = "a < b";
` **After**: `const x = " ";
` (spaces preserve character positions) Now the regex won't mistake `<` inside the string for a tag. ### Edge Cases Handled #### Multi-line Opening Tags ```jsx ``` The algorithm **extracts the full opening tag** first (across lines), then starts the stack search from after the `>`. #### Nested Same-Tag Elements ```jsx
Nested div
Another nested div
``` The **stack tracks depth**: - Open outer `
` → stack: `[div]` - Open nested `
` → stack: `[div, div]` - Close nested `
` → stack: `[div]` - Open second nested `
` → stack: `[div, div]` - Close second nested `
` → stack: `[div]` - Close outer `
` → stack: `[]` → **Match found!** #### JSX Expressions ```jsx
{value > 0 && Positive: {value}}
``` The algorithm handles: - `>` inside expressions (removed by `removeStringLiterals`) - Nested tags inside `{}` - Multiple expressions *** ## Step 7: Element Registration ### FileStateManager Registration Once the source file is found and keakId is injected (if needed), the element is **registered** in FileStateManager: ```typescript function handleElementSelected(elementData: ElementData) { // 1. Resolve source file const { filePath } = await resolveElementSource(elementData, projectRoot); // 2. Read file content const fileContent = await electronAPI.readFile(filePath); // 3. Check if chat exists (restore snapshot if so) const persistedChat = await chatPersistence.loadChat(keakId); if (persistedChat?.elementSnapshot) { // CRITICAL: Restore BEFORE registration fileStateManager.restoreSnapshot(keakId, filePath, persistedChat.elementSnapshot); } // 4. Register element in FileStateManager fileStateManager.registerElement(keakId, filePath, fileContent); // 5. Register in Zustand store useKeakStore.getState().registerElement({ keakId, filePath, elementCode: elementData.outerHTML }); // 6. Select element (opens chat tab) useKeakStore.getState().selectElement(keakId); } ``` **Why snapshot restoration before registration?** See [Core Concepts: Snapshot System](./core-concepts.md#snapshot-system-preventing-data-corruption) for the variant-as-control bug explanation. *** ## Step 8: Chat Initialization ### Loading Conversation History **Priority-based hydration**: ```typescript async function initializeChatForElement(keakId: string) { // Priority 1: ChatPersistence (file-based, most durable) const persisted = await chatPersistence.loadChat(keakId); if (persisted) { // Restore full state setMessages(persisted.messages); setVariants(persisted.variants); setUserGoal(persisted.userGoal); setChatId(persisted.chatId); // ... all state return; // Done! } // Priority 2: In-memory conversationHistory (survives tab switch) const inMemory = conversationHistory.get(elementId); if (inMemory) { setMessages(inMemory.messages); setVariants(inMemory.variants); return; // Done! } // Priority 3: Fresh chat setMessages([{ role: 'assistant', content: 'Tell me what you'd like to improve about this element.' }]); } ``` **Hydration ensures**: - Previous conversations resume seamlessly - No data loss from app restarts - Fast tab switching (in-memory cache) ### Telemetry Initialization Track when a chat starts: ```typescript // use-cro-telemetry.ts useEffect(() => { if (!selectedElement?.keakId) return; const initTelemetry = async () => { // Check if chat already exists in DB const existingChat = await telemetry.lookupChat(projectId, keakId); if (existingChat) { // Resume existing chat setChatId(existingChat.id); } else { // Create new chat const chat = await telemetry.trackChatStart({ projectId, keakId, elementSelector: generateSelector(selectedElement), originalCode: selectedElement.outerHTML }); setChatId(chat.id); } }; initTelemetry(); }, [selectedElement]); ``` This links IDE conversations to the telemetry database for analytics. *** ## The Complete Flow ### End-to-End Sequence ``` 1. User clicks ", tagName: "button", textContent: "Sign Up", attributes: { "data-keak-src": "components/hero.tsx:42:8" } } ↓ 3. WebSocket message sent to IDE ↓ 4. IDE extracts keakId from attributes OR generates: keakId = "sign-up-button" ↓ 5. Resolve source file: - Try data-keak-src → "components/hero.tsx:42:8" ✅ - File exists at projectRoot/components/hero.tsx ↓ 6. Read source file, check if keakId exists - Search for: data-keak-id="sign-up-button" - Not found → Inject it! ↓ 7. Inject data-keak-id into source: ", tagName: "button", textContent: "Sign Up" }); ``` This enables: - Tab management (openTabs array) - Quick element lookup - Monaco editor highlighting See [State Management](./state-management) for data flow. *** ## Related Documentation - **[Core Concepts](./core-concepts)** - KeakId system, source resolution - **[Babel Integration](./babel-integration)** - Plugin setup and configuration - **[Preview System](./preview-system)** - How boundaries are used for swapping - **[Architecture](./architecture)** - Overall system design --- /docs/extension-ide/ide/experiment-workflow # Experiment Workflow **The Complete Story**: From clicking an element to launching a production A/B test. This document walks through the entire user journey, showing how all the systems work together. *** ## The 5-Step Journey ``` 1. Element Selection → Click element, drawer opens with history 2. Variant Generation → Enter goal, AI creates optimized variant 3. Preview & Iterate → Toggle preview, edit code, refine 4. Apply Variant → Wrap in experiment component 5. Launch Experiment → Configure and sync to Dashboard ``` **Total time**: 2-5 minutes from idea to live experiment. *** ## Step 1: Element Selection ### User Story > "I want to optimize my hero CTA button to increase sign-ups." ### What Happens **User action**: Clicks the `` in preview browser. **System response**: ``` 1. Preview browser captures element data { outerHTML: "", tagName: "button", textContent: "Sign Up", attributes: { "data-keak-src": "components/hero.tsx:42:8" } } 2. WebSocket sends to IDE 3. IDE resolves source file - Parse data-keak-src → "components/hero.tsx:42:8" - File found at projectRoot/components/hero.tsx ✅ 4. IDE generates keakId - From textContent: "sign-up-button" 5. IDE checks if keakId exists in source - Search for: data-keak-id="sign-up-button" - Not found → Inject it! 6. Inject keakId into source (line 42): " - Variant: "" 9. Register element in FileStateManager - Knows variant is on disk (from snapshot) - Captures variant correctly (not as control) 10. Register in Zustand store - Add to elements registry - Add to openTabs: ["sign-up-button"] - Set activeKeakId: "sign-up-button" 11. Track in telemetry - POST /telemetry/chats/start - Resume existing chat (chatId already exists) 12. CRO Drawer opens - Displays previous conversation - Shows 2 generated variants - Variant 2 currently on disk (blue banner) - AI suggests new prompts ``` **Time**: ~100-300ms from click to drawer open. **User sees**: Full conversation history, previous variants, preview indicator. See [Element Tracking](./element-tracking) for implementation details. *** ## Step 2: Variant Generation ### User Story > "Let me try a third approach. Add urgency and emphasize the free trial." ### What Happens **User action**: 1. Types: "Add urgency and emphasize free trial" 2. Clicks "Generate" **System response**: ``` 1. Determine generation type - variants.length = 2 (previous variants exist) - isRetry = false - generationType = 'FOLLOWUP' 2. Mark previous variant - chatLastVariantId = variant 2's ID - POST /telemetry/variants/followup - Variant 2 outcome: SKIPPED (not replaced, just building on it) 3. Update UI state - setIsGenerating(true) - useKeakStore: setGenerating(true, 'reasoning') - Show "Analyzing element..." loader 4. Add user message - setMessages([...messages, { role: 'user', content: goal }]) - Appears in chat immediately 5. Track generation start - POST /telemetry/generations/start - Returns generationId for linking 6. Call KeaFab API (streaming) IDE → Dashboard → KeaFab Request: { full_page: "
...full page JSX...
", selected_component: "", // Current variant (not control!) user_prompt: "Add urgency and emphasize free trial", screenshot_url: "https://blob.vercel.com/...", tier: "turbo" } Streaming responses: - { type: 'progress', step: 'router', message: 'Classifying component...' } (~1s) - { type: 'progress', step: 'optimizer', message: 'Optimizing button...' } (~15s) - { type: 'progress', step: 'validator', message: 'Validating code...' } (~3s) - { type: 'complete', data: { analysis, variant, changes, usage } } 7. Track generation complete - POST /telemetry/generations/complete - Includes: tokens, duration, variant code - Returns variantId 8. Update state with variant - setVariants([...variants, variant3]) - setMessages([...messages, assistantMessage]) - conversationHistory.set(elementId, ...) 9. Auto-preview with repair - applier.previewWithRepair(variant3.code) - No syntax errors → Success! - FileStateManager.applyPreview() - Write to disk, dev server reloads 10. Update preview state - setPreviewVariantId(variant3.id) - setHasUnsavedChanges(true) 11. Track variant preview - POST /telemetry/variants/preview - Increment preview_count - Set first_previewed_at 12. Show notification - Native Electron notification: "Variation Ready" 13. Auto-save triggers (500ms) - ChatPersistence.saveChat("sign-up-button", completeState) - Includes variant 3, new messages, updated snapshot - Writes to .keak/history/history.json 14. Clear generating state - setIsGenerating(false) - useKeakStore: setGenerating(false, 'complete') ``` **Time**: ~20-30 seconds from prompt to variant displayed. **User sees**: - Streaming progress messages - New variant appears with CRO analysis: - "COPY: Emphasized free trial benefit" - "STYLE: Added urgency with 'Today' modifier" - "Expected: +15-25% click-through rate" - Variant auto-previewed in browser (blue banner) - Can give feedback, edit, or apply See [Variant Generation](./variant-generation) for implementation details. *** ## Step 3: Preview & Iterate ### User Story > "Hmm, 'Start Your Free Trial Today' is good, but let me tweak it to 'Start Free Today'." ### What Happens **User action**: 1. Opens variant code in Monaco editor (or VS Code) 2. Edits: `"Start Your Free Trial Today"` → `"Start Free Today"` 3. Saves file (Cmd+S) **System response**: ``` 1. File write triggers dev server HMR - Preview browser reloads - Shows "Start Free Today" (edited variant) 2. Next preview toggle captures edit - User clicks "Hide Preview" - hidePreview() reads disk - Captures: "" - Stores in snapshot.variantCode - Also stores in editedVariantCode Map (Tier 3 state) 3. Auto-save triggers - editedVariantCode changed - useEffect fires (500ms debounce) - ChatPersistence.saveChat(keakId, { editedVariantCode: { [v3.id]: "..." } }) 4. Telemetry tracks edit - POST /telemetry/variants/preview (detects code change) - Sets has_edits: true - Sets final_code: "" - Outcome: EDITED (was PENDING) ``` **User can now**: - Click "Preview" again → Shows edited version - Generate another variant → Build on edited version - Apply edited version → Uses edited code, not original **Key feature**: Edits are preserved even if user: - Hides preview - Switches tabs - Closes IDE - Edits in VS Code See [Preview System](./preview-system) for swap mechanics. *** ## Step 4: Apply Variant ### User Story > "Perfect! Let's ship this as an A/B test." ### What Happens **User action**: Clicks "Apply Variant" button on variant 3 (edited version). **System response**: ``` 1. Validation check - validationResults.length === 0 ✅ - Button is enabled 2. Apply preview (write variant to disk) - codeToApply = editedVariantCode.get(v3.id) || v3.code - applier.preview(codeToApply) - FileStateManager writes variant to disk - Dev server reloads 3. Track telemetry - POST /telemetry/variants/preview (final preview before apply) - POST /telemetry/variants/accept - Outcome: ACCEPTED - Track in PostHog: 'variant_accepted' 4. Generate experiment key - experimentKey = `cro-${Date.now()}` - Example: "cro-1706123456789" 5. Wrap in component - Call onApplyVariant(originalCode, variantCode, experimentKey) - This triggers wrapFlag.ts Before (variant on disk): After (experiment wrapper): import { Experiment, Variant } from '@keak/sdk'; 6. Write to source file - File now contains both control and variant - Dev server reloads - Experiment is live locally! 7. Update project state - useProject: add to experiments array - Experiment scanner detects new experiment 8. Close CRO drawer - Drawer slides closed - User returns to preview ``` **Time**: ~1-2 seconds from click to experiment live. **User sees**: Drawer closes, preview shows control version (50/50 traffic). **Next step**: Configure and launch experiment to production. *** ## Step 5: Launch Experiment ### User Story > "Now I need to configure this experiment and push it to production." ### What Happens **User action**: 1. Opens "Experiments" tab (right panel) 2. Sees the new experiment: `cro-1706123456789` 3. Clicks "Configure" button **Configuration Dialog**: ```typescript {/* Experiment Name */} {/* Traffic Allocation */}
Control: {trafficSplit}% | Treatment: {100 - trafficSplit}%
{/* Goal Selection */} {/* Page View Event */}
``` **User fills in**: - Name: "Hero CTA urgency test" - Traffic: 50% control, 50% treatment - Goal: "Sign up button clicked" (conversion event) - Page view: "Home page viewed" (eligibility) **System creates experiment**: ```typescript async function handleCreateExperiment() { try { // 1. Create experiment group on Dashboard const response = await fetch(`${dashboardUrl}/experiments/create`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ domain_id: currentProject.domain_id, name: experimentName, experiment_key: experimentKey, traffic_allocation: { control: trafficSplit, treatment: 100 - trafficSplit }, goal_event_id: goalId, page_view_event_id: pageViewId, variants: [ { name: 'control', code: controlCode, element_id: keakId }, { name: 'treatment', code: variantCode, element_id: keakId } ] }) }); const { test_id } = await response.json(); // 2. Link variant to experiment in telemetry await telemetry.linkVariantToExperiment(variantId, test_id); // 3. Update local experiment state useProject.getState().addExperiment({ id: test_id, key: experimentKey, name: experimentName, status: 'running', created_at: Date.now() }); // 4. Track in PostHog posthog.capture('experiment_launched', { experiment_id: test_id, experiment_key: experimentKey, element_type: 'button', variant_count: 2 }); // 5. Show success message toast.success('Experiment launched! Live on production.'); } catch (error) { console.error('[Launch] Failed:', error); toast.error(`Failed to launch: ${error.message}`); } } ``` **Dashboard creates**: - Experiment record in `tests` table - Traffic allocation rules - Goal assignment - Variant metadata **Result**: Experiment is **live on production** (once code is deployed). *** ## Complete Flow Diagram ```mermaid sequenceDiagram participant User participant Browser as Preview Browser participant IDE as Keak-Code IDE participant FS as File System participant Dashboard as Dashboard API participant KeaFab as KeaFab API participant TelemetryDB as Telemetry DB participant DevServer as Dev Server Note over User,DevServer: Step 1: Element Selection User->>Browser: Clicks " } }, openTabs: ["sign-up-button"], activeKeakId: "sign-up-button" } ``` **Tier 2 (ChatPersistence)** - `.keak/history/history.json`: ```json { "chats": { "sign-up-button": { "messages": [ /* 4 messages from previous session */ ], "variants": [ /* 2 variants from previous session */ ], "elementSnapshot": { "controlCode": "", "variantCode": "", "activePreview": true } } } } ``` **Tier 3 (React state)**: ```typescript messages: [ /* hydrated from Tier 2 */ ] variants: [ /* 2 variants hydrated */ ] previewVariantId: "v2" // Variant 2 on disk chatId: "uuid-123" ``` **FileStateManager**: ```typescript elementSnapshots.get("sign-up-button"): { originalCode: "", variantCode: "", activePreview: true } ``` ### After Step 2 (Variant Generated) **Tier 3 (React state)** - updated: ```typescript messages: [ /* previous 4 + 2 new (user + assistant) */ ] variants: [ /* previous 2 + variant 3 */ ] previewVariantId: "v3" // New variant chatLastVariantId: "uuid-variant-3" ``` **Tier 2 (ChatPersistence)** - auto-saved (500ms): ```json { "chats": { "sign-up-button": { "messages": [ /* 6 messages */ ], "variants": [ /* 3 variants */ ], "elementSnapshot": { "controlCode": "", "variantCode": "", "activePreview": true }, "previewVariantId": "v3" } } } ``` **FileStateManager** - updated: ```typescript elementSnapshots.get("sign-up-button"): { originalCode: "", variantCode: "", // V3 code activePreview: true } ``` ### After Step 3 (User Edits Variant) **Tier 3 (React state)** - edit tracked: ```typescript editedVariantCode: Map { "v3" => "" // User's edit } ``` **FileStateManager** - edit captured: ```typescript elementSnapshots.get("sign-up-button"): { originalCode: "", variantCode: "", // Edited version activePreview: false // Hide preview was clicked } ``` ### After Step 4 (Variant Applied) **Source file** - experiment wrapper: ```jsx // components/hero.tsx import { Experiment, Variant } from '@keak/sdk'; const Hero = () => (

Welcome

); ``` **Tier 1 (useProject)** - experiments updated: ```typescript { experiments: [ { key: "cro-1706123456789", name: "Hero CTA urgency test", status: "draft", // Not yet launched to Dashboard filePath: "components/hero.tsx" } ] } ``` ### After Step 5 (Experiment Launched) **Dashboard `tests` table** - new record: ```sql INSERT INTO tests ( domain_id, name, experiment_key, control_variant, treatment_variant, traffic_allocation, goal_event_id, status ) VALUES ( 'domain-uuid', 'Hero CTA urgency test', 'cro-1706123456789', '{"code": ""}', '{"code": ""}', '{"control": 50, "treatment": 50}', 'event-uuid', 'running' ); ``` **Telemetry `variants` table** - linked: ```sql UPDATE variants SET experiment_id = 123 WHERE id = 'uuid-variant-3'; ``` **Result**: Variant is now linked to production experiment for analytics. *** ## Chat History & Tabs ### Multiple Concurrent Conversations Users can have **multiple chat tabs open** simultaneously: ```typescript // User's openTabs after optimizing 3 elements openTabs: [ "hero-cta-button", "pricing-table-header", "footer-signup-link" ] activeKeakId: "hero-cta-button" // Currently active tab ``` **Each tab**: - Independent conversation history - Independent variant list - Independent preview state - Independent FileStateManager snapshot **Switching tabs**: ``` User clicks "pricing-table-header" tab ↓ useKeakStore.selectElement("pricing-table-header") ↓ activeKeakId changes ↓ useEffect in CRODrawer fires ↓ Load conversation from ChatPersistence ↓ Hydrate React state ↓ FileStateManager switches context ↓ UI updates to show pricing table conversation ``` **Time**: ~10-50ms for tab switch (fast, uses Priority 1 or 2 hydration). ### History Panel **Browsing past conversations**: ```typescript // User clicks "History" view toggle activeView: 'history' // HistoryPanel loads all chats const allChats = await chatPersistence.loadHistory(); // Display sorted by timestamp (newest first) { allChats.map(chat => ( handleSelectFromHistory(chat.keakId)} /> )) } ``` **Clicking a history item**: - Opens as new tab - Loads full conversation - Restores preview state - Can continue optimizing *** ## Experiment Code Structure ### Wrapper Component **Generated by**: `utils/wrapFlag.ts` **Purpose**: A/B test framework integration via `@keak/sdk`. **Structure**: ```jsx import { Experiment, Variant } from '@keak/sdk'; {/* Original element code */} {/* Generated variant code */} ``` **Runtime behavior**: - SDK checks user's variant assignment (via cookie or localStorage) - Renders appropriate variant (control or treatment) - Tracks impressions and goal events - Sends data to Dashboard for analysis **Why this structure?** - ✅ **Both versions in one file** - Easy to review, no file duplication - ✅ **Type-safe** - TypeScript knows both variants exist - ✅ **Reviewable** - Clear diff in PR - ✅ **Reversible** - Easy to remove experiment wrapper ### Import Injection `wrapFlag.ts` **automatically injects** the import: ```typescript function wrapInExperiment(sourceCode: string, experimentData): string { // Parse source file with ts-morph const sourceFile = project.createSourceFile('temp.tsx', sourceCode); // Check if import already exists const hasImport = sourceFile.getImportDeclaration( decl => decl.getModuleSpecifierValue() === '@keak/sdk' ); if (!hasImport) { // Add import at top sourceFile.addImportDeclaration({ moduleSpecifier: '@keak/sdk', namedImports: ['Experiment', 'Variant'] }); } // Find element and wrap // ... (complex AST manipulation) return sourceFile.getFullText(); } ``` **Fallback**: If ts-morph fails (rare), use string manipulation with regex. *** ## Real-World Example ### Complete Timeline **9:00 AM** - User opens Keak-Code IDE - Project loaded: "my-saas-app" - Dev server starts on localhost:3000 - No conversations yet **9:05 AM** - User clicks hero CTA button - CRO drawer opens - AI suggests: "Increase conversions with urgency" - User clicks suggestion **9:06 AM** - Variant 1 generated - Analysis: "Button copy lacks urgency and specificity" - Variant: "Get Started Free → Sign Up" - Auto-previewed in browser **9:07 AM** - User provides feedback - Thumbs up on variant - Types follow-up: "Add time-based urgency" **9:08 AM** - Variant 2 generated - Builds on variant 1 - Variant: "Start Your Free Trial Today" - Auto-previewed **9:09 AM** - User edits in Monaco - Changes to: "Start Free Today" - Saves, sees in preview **9:10 AM** - User applies variant - Clicks "Apply Variant" - Experiment wrapper added to source - Dev server reloads, experiment live locally **9:12 AM** - User configures experiment - Name: "Hero CTA urgency test" - Traffic: 50/50 - Goal: "Sign up clicked" - Clicks "Launch" **9:13 AM** - Experiment synced to Dashboard - Dashboard shows running experiment - Variant linked in telemetry - Ready for production deployment **9:14 AM** - User commits and pushes - `git add components/hero.tsx` - `git commit -m "Add hero CTA urgency test"` - `git push` **9:15 AM** - Experiment live on production - Users see 50% control, 50% treatment - Dashboard tracks conversions - Winner will be determined by SPRT analysis **Total time**: 15 minutes from idea to production experiment. *** ## Multi-Element Workflows ### Scenario: Optimizing Hero Section **User goal**: Optimize entire hero (headline + subheading + CTA). **Workflow**: ``` 1. Select hero headline - Generate variant - Preview, iterate - Apply variant 2. Select hero subheading - Generate variant (context-aware, knows headline changed) - Preview, iterate - Apply variant 3. Select hero CTA - Generate variant (context-aware, knows headline + subheading changed) - Preview, iterate - Apply variant 4. Create experiment group - Group all 3 experiments - Configure as single test - Launch together ``` **conversationHistory Map** tracks all 3 conversations: ```typescript conversationHistory: Map { "h1-create-stunning" => { messages, variants, originalSource }, "p-transform-your" => { messages, variants, originalSource }, "button-sign-up" => { messages, variants, originalSource } } ``` This enables **context-aware suggestions** across elements. *** ## Tips & Best Practices ### 1. Use AI Suggestions **Don't overthink prompts**. The AI suggestions are optimized for CRO. **Good**: Click "Increase conversions with urgency" (AI suggestion) **Bad**: "Make the button blue and add a shadow" (too specific, ignores CRO) ### 2. Iterate with Feedback **Use multi-turn** to refine: 1. Generate initial variant 2. Give feedback (thumbs up/down) 3. Provide follow-up instructions 4. Repeat until satisfied **Don't**: Regenerate from scratch each time (loses context). ### 3. Edit Before Applying **Preview lets you edit**. Polish the variant before making it permanent. **Common edits**: - Shorten copy - Adjust wording - Add brand-specific terms - Fix spacing/formatting ### 4. Review in Multiple Devices Use the **device toggle** in preview: - Desktop - Tablet - Mobile - Responsive Ensure variant looks good on all screen sizes. ### 5. Check Validation Results **Before applying**, ensure no code quality issues: - keakId preserved - Syntax valid - No placeholders - Assets intact If errors exist, **edit or regenerate** before applying. *** ## Related Documentation - **[Element Tracking](./element-tracking)** - Step 1 implementation - **[Variant Generation](./variant-generation)** - Step 2 implementation - **[Preview System](./preview-system)** - Step 3 implementation - **[State Management](./state-management)** - State flow throughout journey - **[KeaFab API](/docs/ai-infrastructure/keafab/)** - AI generation pipeline --- /docs/extension-ide/ide/git-integration # Git Integration **Version Control for A/B Tests**: How Keak-Code integrates with Git for project import, experiment tracking, and winner deployment. *** ## Features Overview Keak-Code provides Git integration for: 1. **Project Import** - Clone repositories from GitHub 2. **Experiment Commits** - Track experiment changes in git history 3. **PR Creation** - Deploy winning variants via pull requests 4. **Branch Management** - Isolate experiment changes **Not a full Git client** - For complex operations, use external tools (VS Code, GitHub Desktop, CLI). *** ## Project Import from GitHub ### OAuth Flow **User action**: Click "Import from GitHub" in WelcomeScreen. **System flow**: ```typescript // 1. Initiate OAuth const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&scope=repo`; openBrowser(authUrl); // 2. User authorizes on GitHub // 3. GitHub redirects to callback URL // 4. Dashboard exchanges code for access token // 5. IDE receives token via deep link app.on('open-url', async (event, url) => { // url: keak://github-callback?code=abc123&state=xyz const { code, state } = parseUrl(url); // Exchange code for token const { access_token } = await fetch(`${dashboardUrl}/github/exchange`, { method: 'POST', body: JSON.stringify({ code, state }) }); // Store token useGitHub.getState().setAccessToken(access_token); // Show repository list showRepositoryPicker(); }); ``` ### Repository Selection ```typescript // Load user's repositories const repos = await fetch('https://api.github.com/user/repos', { headers: { 'Authorization': `Bearer ${accessToken}` } }); // User selects repository // Clone repository async function handleSelectRepo(repo) { const cloneUrl = repo.clone_url; const localPath = path.join(os.homedir(), 'keak-projects', repo.name); // Use Electron git (dugite or simple-git) await git.clone(cloneUrl, localPath, { onProgress: (progress) => { setCloneProgress(progress.percentage); } }); // Open project await openProject(localPath); } ``` **User sees**: - Repository list from their GitHub account - Clone progress bar - Project opens automatically after clone *** ## Experiment Commits ### Committing Experiments **Best practice**: Commit experiments separately from feature work. **Example commit message**: ``` Add hero CTA urgency test (cro-1706123456789) Testing "Start Free Today" vs "Sign Up" to increase conversions. Generated via Keak-Code AI. Co-authored-by: Keak AI ``` **Manual commit** (via Terminal tab or external Git client): ```bash git add components/hero.tsx git commit -m "Add hero CTA urgency test (cro-1706123456789)" git push origin main ``` ### Experiment Key in Commit Message **Why include experiment key?** - ✅ **Searchable** - `git log --grep="cro-1706123456789"` - ✅ **Traceable** - Links commit to Dashboard experiment - ✅ **Reversible** - Easy to find and revert if needed **Format**: `(cro-timestamp)` in commit message or branch name. *** ## Pull Request Creation ### Winner Selection Flow **Scenario**: Experiment ran for 2 weeks, treatment won with 95% confidence. **User action**: 1. Opens Experiments panel 2. Selects experiment: "Hero CTA urgency test" 3. Clicks "Select Winner" dropdown 4. Chooses "Treatment" (Start Free Today) 5. Clicks "Create PR" ### What Happens **System flow**: ```typescript async function handleCreateWinnerPR(experiment, winner: 'control' | 'treatment') { try { // 1. Get experiment details const { experimentKey, filePath, controlCode, treatmentCode } = experiment; const winnerCode = winner === 'control' ? controlCode : treatmentCode; // 2. Read current file const currentContent = await electronAPI.readFile(filePath); // 3. Remove experiment wrapper, keep winner const updatedContent = removeExperimentWrapper( currentContent, experimentKey, winnerCode ); // 4. Create branch const branchName = `keak-winner-${experimentKey}`; await git.checkoutBranch(branchName); // 5. Write winner to file await electronAPI.writeFile(filePath, updatedContent); // 6. Commit change await git.commit(`Apply ${winner} variant from ${experiment.name} (${experimentKey})`); // 7. Push branch await git.push('origin', branchName); // 8. Create PR via GitHub API const pr = await fetch('https://api.github.com/repos/:owner/:repo/pulls', { method: 'POST', headers: { 'Authorization': `Bearer ${githubToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title: `Apply winner: ${experiment.name}`, head: branchName, base: 'main', body: `## Experiment Results **Experiment**: ${experiment.name} (${experimentKey}) **Winner**: ${winner.toUpperCase()} **Confidence**: 95% **Lift**: +${experiment.lift}% conversion rate This PR applies the winning variant from our A/B test. ### Changes ${winner === 'treatment' ? treatmentCode : 'Reverted to control'} ### Testing - ✅ Experiment ran for ${experiment.duration} days - ✅ ${experiment.impressions} impressions - ✅ Statistical significance reached 🤖 Generated via [Keak-Code](https://keak.com)` }) }); // 9. Show PR URL const prUrl = pr.data.html_url; showNotification(`PR created: ${prUrl}`); openBrowser(prUrl); } catch (error) { console.error('[PR Creation] Failed:', error); showError(`Failed to create PR: ${error.message}`); } } ``` ### Removing Experiment Wrapper **Before** (with wrapper): ```jsx import { Experiment, Variant } from '@keak/sdk'; ``` **After** (treatment won): ```jsx ``` **Implementation** (`utils/applyWinner.ts`): ```typescript function removeExperimentWrapper( sourceCode: string, experimentKey: string, winnerCode: string ): string { // Parse with ts-morph const sourceFile = project.createSourceFile('temp.tsx', sourceCode); // Find component const experimentElement = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement) .find(el => { const name = el.getOpeningElement().getTagNameNode().getText(); const nameAttr = el.getOpeningElement().getAttribute('name'); return name === 'Experiment' && nameAttr?.getInitializer()?.getText() === `"${experimentKey}"`; }); if (!experimentElement) { throw new Error(`Experiment ${experimentKey} not found in source`); } // Replace entire with winner code experimentElement.replaceWithText(winnerCode); // Remove import if no more experiments const hasOtherExperiments = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement) .some(el => el.getOpeningElement().getTagNameNode().getText() === 'Experiment'); if (!hasOtherExperiments) { const importDecl = sourceFile.getImportDeclaration('@keak/sdk'); importDecl?.remove(); } return sourceFile.getFullText(); } ``` **Fallback**: If ts-morph fails, use regex-based replacement. *** ## Branch Management ### Experiment Branches **Optional workflow**: Create branch per experiment. **Manual process**: ```bash # Create experiment branch git checkout -b experiment/hero-cta-urgency # Make changes (apply variant) # ... use Keak-Code to apply variant ... # Commit git add components/hero.tsx git commit -m "Add hero CTA urgency test" # Push git push origin experiment/hero-cta-urgency # Create PR manually or via Keak-Code ``` **Benefits**: - ✅ **Isolation** - Experiment changes don't affect main branch - ✅ **Reviewable** - Team can review before merging - ✅ **Reversible** - Delete branch if experiment fails **Trade-offs**: - ❌ **More complex** - Need to manage branches - ❌ **Merge conflicts** - If main branch changes **Recommended**: Only for large experiments affecting multiple files. *** ## Git Status in UI ### Status Indicator **Top bar** shows git status: ```typescript
{gitStatus.branch && (
{gitStatus.branch}
)} {gitStatus.hasChanges && (
{/* Orange dot */} {gitStatus.modifiedCount} changes
)}
``` **Example**: "🌿 main • 3 changes" ### File Tree Indicators **FileTree component** shows git status per file: ```typescript
{file.name} {file.gitStatus === 'modified' && (
M
)} {file.gitStatus === 'added' && (
A
)}
``` **Statuses**: M (modified), A (added), D (deleted), ?? (untracked). *** ## GitHub API Integration ### Using GitHub CLI (gh) Keak-Code uses **GitHub CLI** for PR operations: ```bash # Install gh CLI (if not present) npm install -g @github/gh # Authenticate gh auth login # Create PR gh pr create \ --title "Apply winner: Hero CTA urgency test" \ --body "..." \ --base main \ --head keak-winner-cro-1706123456789 ``` **Alternative**: REST API (if gh CLI not available). ### PR Template **Auto-generated PR body**: ````markdown ## Experiment Results **Experiment**: Hero CTA urgency test (cro-1706123456789) **Winner**: TREATMENT **Confidence**: 95% **Lift**: +22% conversion rate **Duration**: 14 days **Impressions**: 12,458 ## Changes **Before** (Control): ```jsx ```` **After** (Treatment): ```jsx ``` ## Testing - ✅ Experiment ran for 14 days - ✅ 12,458 impressions, 1,247 conversions - ✅ Statistical significance reached (p < 0.05) - ✅ SPRT analysis confirmed winner ## Deployment Deploy this PR to apply the winning variant to production. 🤖 Generated via [Keak-Code](https://keak.com) ```` **Reviewers** can see: - Full experiment context - Statistical confidence - Before/after code - Test results --- ## Best Practices ### 1. Commit Experiment Separately **Do**: ```bash # Commit experiment addition git add components/hero.tsx git commit -m "Add hero CTA urgency test" # Later: commit winner selection git add components/hero.tsx git commit -m "Apply winner from hero CTA urgency test (+22% lift)" ```` **Don't**: ```bash # Mixed commit (confusing) git add components/hero.tsx components/pricing.tsx git commit -m "Add experiment and fix pricing bug" ``` ### 2. Use Descriptive Experiment Keys **Experiment keys** are timestamps by default (`cro-1706123456789`). **Better**: Include in commit message for context: ``` Add hero CTA urgency test (cro-1706123456789) Testing time-based urgency ("Start Free Today") vs generic CTA ("Sign Up"). ``` ### 3. Document Rationale in PR **Include in PR description**: - Why this experiment was run - What hypothesis was tested - What the results showed - Why winner was selected **Example**: ```markdown ## Hypothesis Users are unaware the trial is free, creating friction in sign-up flow. ## Test Treatment emphasizes "Free" and adds urgency with "Today". ## Results Treatment increased sign-ups by 22% with 95% confidence. ## Decision Apply treatment to capture lift permanently. ``` ### 4. Tag Experiment Commits **Create git tags** for experiments: ```bash git tag -a cro-hero-cta-v1 -m "Hero CTA urgency test launched" git tag -a cro-hero-cta-winner -m "Treatment won (+22% lift)" git push --tags ``` **Enables**: - Quick navigation (`git checkout cro-hero-cta-v1`) - Timeline visualization - Experiment archaeology *** ## Git Workflow Example ### Complete Flow **Week 1: Add Experiment** ```bash # On main branch git status # On branch main # nothing to commit, working tree clean # Use Keak-Code to add experiment # components/hero.tsx is modified (experiment wrapper added) git status # On branch main # Changes not staged for commit: # modified: components/hero.tsx git add components/hero.tsx git commit -m "Add hero CTA urgency test (cro-1706123456789) Testing time-based urgency vs generic CTA. Generated via Keak-Code AI. Co-authored-by: Keak AI " git push origin main ``` **Week 3: Winner Selected** ```bash # Treatment won! Apply via Keak-Code PR creation # Keak-Code creates: # - Branch: keak-winner-cro-1706123456789 # - Commit: "Apply treatment variant from hero CTA urgency test" # - PR: "Apply winner: Hero CTA urgency test" # Review PR on GitHub # Merge when approved ``` *** ## Winner Application ### Automatic Winner Application **Location**: Experiments panel → Select experiment → "Select Winner" dropdown → "Create PR". **What gets created**: 1. **Branch**: `keak-winner-${experimentKey}` 2. **Commit**: Removes experiment wrapper, keeps winner 3. **PR**: Includes experiment results, before/after code **Example diff**: ```diff // components/hero.tsx - import { Experiment, Variant } from '@keak/sdk'; const Hero = () => (

Welcome

- - - - - - -
); ``` **Clean, reviewable diff** showing exactly what changed. ### Manual Winner Application **If PR creation fails** (e.g., no GitHub token), apply manually: ```bash # 1. Find experiment in source grep -r "cro-1706123456789" src/ # 2. Edit file, remove wrapper, keep winner # ... manual edit ... # 3. Commit git add components/hero.tsx git commit -m "Apply treatment from hero CTA test (+22% lift)" # 4. Push git push origin main ``` *** ## .gitignore Configuration ### Recommended .gitignore ```gitignore # Keak-Code IDE files .keak/ # Node modules node_modules/ # Build output .next/ dist/ build/ # Environment files .env.local .env.production.local # OS files .DS_Store Thumbs.db ``` **Why ignore `.keak/`?** - **Privacy** - Contains conversation history with prompts - **Size** - Can grow to several MB - **Local** - Project-specific, not team-specific - **Regenerable** - Can be recreated (unlike source code) **Alternative**: Commit `.keak/` if team wants shared conversation history. *** ## Git Operations in IDE ### Terminal Tab **Access git commands** via integrated terminal: ```typescript { // Execute shell command exec(cmd, { cwd: projectRoot }); }} /> ``` **User can run**: - `git status` - `git log` - `git diff` - `git commit` - `git push` - etc. **Output displayed** in terminal panel. ### File Diff on Hover **FileTree shows diff on hover** (if file modified): ```typescript }>
{file.name} M
// DiffPreview component function DiffPreview({ file }) { const diff = useGitDiff(file.path); return (
      {diff.split('\n').map((line, i) => (
        
{line}
))}
); } ``` **User sees**: Inline diff preview without opening file. *** ## GitHub Integration Limitations **What Keak-Code does NOT provide**: - ❌ Full git client (use VS Code or GitHub Desktop) - ❌ Merge conflict resolution - ❌ Rebase, cherry-pick, stash - ❌ Git hooks management - ❌ Submodule support **For complex git operations**, use external tools. **Keak-Code focuses on**: - ✅ Project import (OAuth + clone) - ✅ Winner PR creation (automated) - ✅ Basic status display - ✅ Terminal access for manual commands *** ## Related Documentation - **[Experiment Workflow](./experiment-workflow)** - Complete experiment lifecycle - **[Setup](./setup)** - GitHub OAuth setup - **[Architecture](./architecture)** - Git integration architecture --- /docs/extension-ide/ide/preview-system # Preview System **The Innovation**: How Keak-Code lets you edit both control and variant versions without losing a single change. **Reference**: Based on `.context/keak-code-preview.txt` design document. *** ## The Problem We're Solving Imagine you're A/B testing a button. You have two versions: - **Control**: `` (original) - **Variant**: `` (AI-generated) **You want to**: - Preview the variant in your app - Edit the variant ("Get Started Free" → "Start Your Free Trial") - Go back to control - Edit the control ("Sign Up" → "Join Now") - Switch back to variant - **Keep all edits** to both versions **The challenge**: Only one version can exist on disk at any time. How do you track edits to both? **Bad solutions**: - ❌ **Two files** - `hero.tsx` and `hero-variant.tsx` → maintenance nightmare - ❌ **Git branches** - One per variant → branch explosion - ❌ **File watchers** - Continuous diffing → complex, resource-intensive - ❌ **Undo stack** - Fragile, doesn't survive restarts **Our solution**: ✅ **Just-in-time bidirectional swapping** *** ## Core Concept: Just-in-Time Swap ### Design Principle From the reference document: > A just-in-time swap system that maintains two independently editable versions of the same element—**Control** (original code) and **Treatment** (generated variant)—with a single source of truth on disk determined by an **activePreview** flag. > > At any moment, only one version is written to the file. The system captures edits only when the user performs an explicit action (swap or apply), not continuously. ### Visual Model ``` ┌──────────────────┐ │ File on Disk │ │ (Single Truth) │ └────────┬─────────┘ │ ┌──────────┴──────────┐ │ │ activePreview = false activePreview = true │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ CONTROL │ │ VARIANT │ │ (original) │ │ (generated) │ └─────────────┘ └─────────────┘ ▲ ▲ │ │ └──────────┬───────────┘ │ Swap captures edits from disk FIRST ``` **Key insight**: The system **always reads from disk first** before swapping. This captures any edits made to the currently active version. ### State Machine ``` Preview: Control → Read disk (capture control edits) → Write variant → activePreview=true Hide: Variant → Read disk (capture variant edits) → Write control → activePreview=false ``` **Properties**: - ✅ **Bidirectional** - Swap in either direction - ✅ **Lossless** - All edits captured and preserved - ✅ **Stateless** - No continuous file watching - ✅ **Persistent** - `activePreview` flag survives app restarts - ✅ **Offline-safe** - Works even if file edited while app closed - ✅ **Editor-agnostic** - Works with VS Code, Vim, any editor *** ## FileStateManager: The Swap Engine ### Architecture **Location**: `services/fileStateManager.ts` **Purpose**: Centralized code swapping with snapshot management. **Data Structures**: ```typescript class FileStateManager { // Map: keakId → element snapshot private elementSnapshots = new Map(); // Map: filePath → in-memory file content (cache) private currentContent = new Map(); } interface ElementSnapshot { keakId: string; filePath: string; originalCode: string; // Control version variantCode: string | null; // Treatment version activePreview: boolean; // true = variant on disk, false = control on disk boundaries?: ElementBoundaries; // Cached for performance } ``` **Why two Maps?** - `elementSnapshots` - Tracks control/variant code per element - `currentContent` - Caches file content to avoid redundant reads ### API Methods ```typescript class FileStateManager { // Initial capture when element selected registerElement( keakId: string, filePath: string, fileContent: string ): void // Preview variant (control → variant) applyPreview( filePath: string, keakId: string, fileContent: string, variantCode: string ): { newContent: string; boundaries: ElementBoundaries } // Hide preview (variant → control) hidePreview( filePath: string, keakId: string, fileContent: string ): { newContent: string; capturedVariantCode: string } // Get both codes for experiment wrapping getCodesForExperiment( filePath: string, keakId: string, fileContent: string ): { controlCode: string; variantCode: string; boundaries: ElementBoundaries } // Restore from persistence (before registerElement) restoreSnapshot( keakId: string, filePath: string, snapshot: { controlCode, variantCode, activePreview } ): void // Get snapshot for persistence getSnapshot(keakId: string): ElementSnapshot | null } ``` *** ## Preview Flow: Control → Variant **User Action**: Click "Preview" button on a variant card. ### High-Level Flow (variantApplier.ts) ```typescript async function previewVariant( sourceMapping: SourceMapping, variantCode: string, keakId: string, onWriteFile: (path: string, content: string) => Promise ): Promise<{ content: string; boundaries: ElementBoundaries; error?: string }> { const { filePath } = sourceMapping; // Read fresh from disk (captures any control edits made since last action) const fileContent = await electronAPI.readFile(filePath); // Apply preview via FileStateManager const { newContent, boundaries } = fileManager.applyPreview( filePath, keakId, fileContent, variantCode ); // Write to disk (triggers dev server HMR) await onWriteFile(filePath, newContent); return { content: newContent, boundaries }; } ``` **Why read from disk?** User might have edited the file in VS Code while IDE was in the background. Reading fresh ensures we capture those edits. ### Core Swap Logic (fileStateManager.ts) ```typescript function applyPreview( filePath: string, keakId: string, fileContent: string, variantCode: string ): { newContent: string; boundaries: ElementBoundaries } { // 1. Get or create snapshot let snapshot = this.elementSnapshots.get(keakId); if (!snapshot) { snapshot = { keakId, filePath, originalCode: '', variantCode: null, activePreview: false }; this.elementSnapshots.set(keakId, snapshot); } // 2. Find element boundaries in current file const lines = fileContent.split('\n'); const boundaries = findElementBoundaries(lines, keakId); // 3. Extract current element code from disk const currentCode = extractElementCode(lines, boundaries); // 4. Capture current code into appropriate slot if (!snapshot.activePreview) { // Currently showing control → capture it snapshot.originalCode = currentCode; } else { // Currently showing previous variant → capture its edits snapshot.variantCode = currentCode; } // 5. Replace element with new variant const newContent = replaceElement(lines, boundaries, variantCode); // 6. Update snapshot state snapshot.variantCode = variantCode; snapshot.activePreview = true; snapshot.boundaries = boundaries; // Cache for performance // 7. Cache in-memory content this.currentContent.set(filePath, newContent); return { newContent, boundaries }; } ``` ### State Transitions **Before preview**: ``` Disk: Snapshot: { originalCode: "", variantCode: null, activePreview: false } ``` **User clicks "Preview"**: ``` 1. Read disk → "" 2. Capture to originalCode (activePreview is false) 3. Write variant to disk 4. Update flag ``` **After preview**: ``` Disk: Snapshot: { originalCode: "", ✅ Preserved! variantCode: "", activePreview: true } ``` ### Example with Edits **Scenario**: User edits control before previewing. **Initial**: ``` Disk: Snapshot: { originalCode: "", variantCode: null, activePreview: false } ``` **User edits** (in VS Code): ``` Disk: ``` **User clicks "Preview"**: ```typescript // 1. Read disk (captures edit!) const currentCode = readDisk(); // → "" // 2. Capture to originalCode snapshot.originalCode = currentCode; // 3. Write variant writeDisk(variantCode); // 4. Update flag snapshot.activePreview = true; ``` **Result**: ``` Disk: // Variant Snapshot: { originalCode: "", ✅ Edit preserved! variantCode: "", activePreview: true } ``` The control edit is **safely captured** even though it was made outside the IDE. *** ## Hide Preview Flow: Variant → Control **User Action**: Click "Hide Preview" button (same button, toggles). ### High-Level Flow (variantApplier.ts) ```typescript async function hidePreview( sourceMapping: SourceMapping, keakId: string, onWriteFile: (path: string, content: string) => Promise ): Promise<{ content: string; capturedVariantCode: string }> { const { filePath } = sourceMapping; // Read fresh from disk (captures any variant edits) const fileContent = await electronAPI.readFile(filePath); // Hide preview via FileStateManager const { newContent, capturedVariantCode } = fileManager.hidePreview( filePath, keakId, fileContent ); // Write control back to disk await onWriteFile(filePath, newContent); return { content: newContent, capturedVariantCode }; } ``` ### Core Swap Logic (fileStateManager.ts) ```typescript function hidePreview( filePath: string, keakId: string, fileContent: string ): { newContent: string; capturedVariantCode: string } { // 1. Verify preview is active const snapshot = this.elementSnapshots.get(keakId); if (!snapshot || !snapshot.activePreview) { throw new Error('No active preview to hide'); } // 2. Find element boundaries (use cached if available) const lines = fileContent.split('\n'); const boundaries = snapshot.boundaries || findElementBoundaries(lines, keakId); // 3. Extract current element code (variant with potential edits) const currentCode = extractElementCode(lines, boundaries); // 4. Capture variant edits snapshot.variantCode = currentCode; // 5. Replace with original control code const newContent = replaceElement(lines, boundaries, snapshot.originalCode); // 6. Update snapshot state snapshot.activePreview = false; // 7. Cache in-memory content this.currentContent.set(filePath, newContent); return { newContent, capturedVariantCode: currentCode }; } ``` ### State Transitions **Before hide**: ``` Disk: // Variant Snapshot: { originalCode: "", variantCode: "", activePreview: true } ``` **User edits variant** (in Monaco): ``` Disk: ``` **User clicks "Hide Preview"**: ```typescript // 1. Read disk (captures variant edit!) const currentCode = readDisk(); // → "" // 2. Capture to variantCode snapshot.variantCode = currentCode; // 3. Write control writeDisk(snapshot.originalCode); // 4. Update flag snapshot.activePreview = false; ``` **After hide**: ``` Disk: // Control restored Snapshot: { originalCode: "", variantCode: "", ✅ Edit preserved! activePreview: false } ``` The variant edit is **safely captured** and can be re-previewed later. *** ## Helper Functions ### Replace Element in Source **Purpose**: Swap element code while preserving surrounding code. ```typescript function replaceElement( lines: string[], boundaries: ElementBoundaries, newCode: string ): string { const { startLine, startColumn, endLine, endColumn } = boundaries; if (startLine === endLine) { // Single-line element lines[startLine] = lines[startLine].substring(0, startColumn) + newCode + lines[startLine].substring(endColumn); } else { // Multi-line element lines[startLine] = lines[startLine].substring(0, startColumn) + newCode; // Remove middle lines lines.splice(startLine + 1, endLine - startLine); } return lines.join('\n'); } ``` **Example** (single-line): ```jsx // Original (line 10) const Hero = () =>

Old

; // Boundaries: { startLine: 10, startColumn: 28, endLine: 10, endColumn: 39 } // Replace with:

New

// Result const Hero = () =>

New

; ``` **Example** (multi-line): ```jsx // Original (lines 10-12) const Hero = () => (

Old Headline

); // Boundaries: { startLine: 11, startColumn: 2, endLine: 13, endColumn: 7 } // Replace with:

New Headline

// Result const Hero = () => (

New Headline

); ``` Note how middle lines are removed, and the new code goes on the first line. ### Extract Element Code **Purpose**: Get element code from boundaries. ```typescript function extractElementCode( lines: string[], boundaries: ElementBoundaries ): string { const { startLine, startColumn, endLine, endColumn } = boundaries; if (startLine === endLine) { // Single-line element return lines[startLine].substring(startColumn, endColumn); } else { // Multi-line element const firstLine = lines[startLine].substring(startColumn); const middleLines = lines.slice(startLine + 1, endLine); const lastLine = lines[endLine].substring(0, endColumn); return [firstLine, ...middleLines, lastLine].join('\n'); } } ``` *** ## Snapshot Persistence & Restoration ### Persistence (Auto-Save) Snapshots are **automatically saved** with chat data: ```typescript // use-cro-state.ts auto-save effect (500ms debounce) useEffect(() => { const timeoutId = setTimeout(() => { // Get latest snapshot from FileStateManager const snapshot = getFileStateManager().getSnapshot(keakId); // Include in ChatPersistence save await chatPersistence.saveChat(keakId, { ...chatState, elementSnapshot: snapshot ? { controlCode: snapshot.originalCode, variantCode: snapshot.variantCode, activePreview: snapshot.activePreview } : null }); }, 500); return () => clearTimeout(timeoutId); }, [keakId, variants, messages, /* all dependencies */]); ``` **Saved to**: `.keak/history/history.json` ```json { "chats": { "hero-cta-button": { "elementSnapshot": { "controlCode": "", "variantCode": "", "activePreview": true }, "...": "other chat data" } } } ``` ### Restoration (On Element Re-Selection) **Critical**: Restore **BEFORE** registering element. ```typescript async function handleElementSelected(elementData) { const { filePath, keakId } = resolveElement(elementData); // 1. Load persisted chat const persistedChat = await chatPersistence.loadChat(keakId); // 2. Restore snapshot FIRST (if exists) if (persistedChat?.elementSnapshot) { fileStateManager.restoreSnapshot(keakId, filePath, { controlCode: persistedChat.elementSnapshot.controlCode, variantCode: persistedChat.elementSnapshot.variantCode, activePreview: persistedChat.elementSnapshot.activePreview }); } // 3. NOW safe to register fileStateManager.registerElement(keakId, filePath, fileContent); } ``` **Why this order?** Without restoration: ``` App restarts with variant on disk registerElement() reads disk → captures variant as control ❌ Original control code lost forever ``` With restoration: ``` restoreSnapshot() tells FileStateManager: activePreview=true (variant is on disk) registerElement() knows to update variantCode slot ✅ Control code preserved in snapshot.originalCode ``` See [Core Concepts: Snapshot System](./core-concepts.md#snapshot-system-preventing-data-corruption) for details. *** ## Edge Cases & Solutions ### Edge Case 1: Switching Between Variants **Scenario**: User previews Variant A, then previews Variant B without hiding A first. **Challenge**: Preserve A's edits while switching to B. **Current Handling**: ```typescript function applyPreview(filePath, keakId, fileContent, newVariantCode) { const snapshot = this.elementSnapshots.get(keakId); // Extract current code from disk const currentCode = extractElementCode(...); if (snapshot.activePreview) { // Currently showing a variant (A) // Capture its edits before switching to B snapshot.variantCode = currentCode; } else { // Currently showing control snapshot.originalCode = currentCode; } // Now replace with new variant (B) const newContent = replaceElement(..., newVariantCode); snapshot.variantCode = newVariantCode; // Store B snapshot.activePreview = true; // Problem: Variant A's edits are in snapshot.variantCode // But this line overwrites it with B! } ``` **Current Limitation**: Only one variant's edits are tracked at a time. The last previewed variant's code is preserved. **Solution** (for future enhancement): ```typescript interface ElementSnapshot { originalCode: string; variants: Map; // Track edits per variant activeVariantId: string | null; } ``` This would enable editing multiple variants independently. ### Edge Case 2: App Restart with Variant on Disk **Scenario**: 1. User previews variant (activePreview=true) 2. App crashes or user closes IDE 3. User reopens IDE and selects same element **Without Snapshot Restoration**: ``` File on disk: (variant) registerElement() reads disk → captures as control ❌ Original control "" lost forever ``` **With Snapshot Restoration**: ``` Load snapshot: { controlCode: "", variantCode: "", activePreview: true } restoreSnapshot() → FileStateManager knows variant is on disk registerElement() reads disk: - Sees activePreview=true - Captures to variantCode slot ✅ - Preserves snapshot.originalCode Result: Both versions intact! ``` ### Edge Case 3: Offline Edits **Scenario**: User previews variant, closes IDE, edits file in VS Code, reopens IDE. **Timeline**: ``` 1. Preview variant (activePreview=true) 2. Close IDE 3. Edit file in VS Code: ↓ 4. Reopen IDE, click "Hide Preview" ``` **Handling**: ```typescript // hidePreview() is called const fileContent = readDisk(); // → "" // snapshot.activePreview is true (from persistence) // So this is a variant edit snapshot.variantCode = fileContent; // Write control back writeDisk(snapshot.originalCode); ``` **Result**: ``` Snapshot: { originalCode: "", variantCode: "", ✅ Offline edit preserved! activePreview: false } ``` **Why this works**: `activePreview` acts as a **bookmark** telling us *which version* those offline edits belong to. ### Edge Case 4: File Edited While Preview Active **Scenario**: Variant is on disk, user edits unrelated parts of the file. **Example**: ```jsx // Before (variant on disk) const Hero = () => { const [state, setState] = useState(false); return (

Variant Text

// Active preview

Some paragraph

); }; ``` **User adds a function**: ```jsx // After edit const Hero = () => { const [state, setState] = useState(false); const handleClick = () => { // ← User added this setState(true); }; return (

Variant Text

Some paragraph

); }; ``` **What happens on hide preview?** ```typescript // 1. Read disk (gets full file with new function) const fileContent = readDisk(); // 2. Extract ONLY the h1 element const boundaries = findElementBoundaries(lines, "hero-headline"); const h1Code = extractElementCode(lines, boundaries); // → "

Variant Text

" // 3. Capture h1 code to variantCode snapshot.variantCode = h1Code; // 4. Replace h1 with control const newContent = replaceElement(lines, boundaries, snapshot.originalCode); // → File now has control h1, but keeps the handleClick function // 5. Write back writeDisk(newContent); ``` **Result**: The `handleClick` function is **preserved**. Only the h1 element is swapped. **Key principle**: Swaps are **element-scoped**, not file-scoped. *** ## Why This Design Works ### Property 1: Bidirectional You can swap in **either direction**: - Control → Variant (preview) - Variant → Control (hide) - Control → Variant again (re-preview) - etc. Each swap captures edits to the current version before writing the other version. ### Property 2: Lossless **All edits are captured** because: - Read from disk before each swap (captures external edits) - Store in appropriate slot (control or variant) - activePreview flag tracks which slot to update **No edits are lost**, regardless of where they're made (IDE, VS Code, Vim). ### Property 3: Stateless **No continuous file watching**. State is captured **only at action time**: - Preview button clicked → Read disk - Hide button clicked → Read disk - Apply button clicked → Read disk **Benefits**: - Simpler implementation (no file watchers) - Lower resource usage (no continuous polling) - More reliable (no race conditions) ### Property 4: Persistent The `activePreview` flag and both code versions are **persisted to disk** (.keak/history/history.json). **Survives**: - App restarts - System reboots - Git commits (history file is in .gitignore) ### Property 5: Offline-Safe `activePreview` acts as a **bookmark** that survives app closure. **Example**: ``` 1. Preview variant (activePreview=true) 2. Close IDE 3. Edit file in VS Code 4. Reopen IDE 5. Hide preview The system knows those edits belong to the variant (activePreview=true) Captures them correctly ✅ ``` ### Property 6: Editor-Agnostic Works with **any text editor**: - Monaco Editor (in IDE) - VS Code - Vim, Emacs, Sublime, etc. As long as edits are saved to disk, the next swap will capture them. *** ## Snapshot Lifecycle ### Complete Lifecycle Diagram ```mermaid stateDiagram-v2 [*] --> NoSnapshot: Element selected (first time) NoSnapshot --> ControlOnDisk: registerElement() ControlOnDisk --> VariantOnDisk: Preview clicked VariantOnDisk --> ControlOnDisk: Hide clicked ControlOnDisk --> Persisted: Auto-save (500ms) VariantOnDisk --> Persisted: Auto-save (500ms) Persisted --> Restored: App restart / Element re-selected Restored --> ControlOnDisk: activePreview=false Restored --> VariantOnDisk: activePreview=true VariantOnDisk --> Applied: Apply variant clicked Applied --> [*]: Experiment wrapping complete ``` ### Snapshot States | State | activePreview | Disk Content | Snapshot State | |-------|---------------|--------------|----------------| | **NoSnapshot** | N/A | Control | Just created, empty | | **ControlOnDisk** | false | Control | Control captured, no variant yet | | **VariantOnDisk** | true | Variant | Both versions captured | | **Persisted** | (preserved) | (preserved) | Saved to .keak/history/ | | **Restored** | (from file) | (depends on flag) | Hydrated from persistence | | **Applied** | N/A | Experiment wrapper | Snapshot no longer needed | *** ## Integration with React State ### Hook Interface: useSimpleVariantApplier **Location**: `hooks/use-variant-applier.ts` **Purpose**: React hook wrapper around variantApplier service. ```typescript function useSimpleVariantApplier(selectedElement) { const [previewActive, setPreviewActive] = useState(false); const [appliedVariants, setAppliedVariants] = useState>(new Set()); const preview = async (variantCode: string) => { const result = await previewVariant( selectedElement.sourceMapping, variantCode, selectedElement.keakId, onWriteFile ); if (!result.error) { setPreviewActive(true); } return result; }; const hide = async () => { const result = await hidePreview( selectedElement.sourceMapping, selectedElement.keakId, onWriteFile ); setPreviewActive(false); return result; }; const apply = async (variantCode: string) => { await applyVariant( selectedElement.sourceMapping, variantCode, selectedElement.keakId ); setAppliedVariants(prev => new Set(prev).add(variantId)); setPreviewActive(false); }; return { preview, hide, apply, previewActive, appliedVariants }; } ``` **Used by**: CRODrawer component ```typescript const applier = useSimpleVariantApplier(selectedElement); // In VariantCard ``` *** ## Preview with Auto-Repair ### The Problem AI-generated code might have **syntax errors**: - Missing closing tags - Unbalanced braces - Invalid JSX expressions **Previewing broken code** → Dev server error → Bad UX. ### The Solution **Auto-repair with GPT** (max 3 attempts): ```typescript async function previewWithRepair( variantCode: string, maxAttempts: number = 3 ): Promise<{ success: boolean; error?: string }> { let currentCode = variantCode; let attempts = 0; while (attempts < maxAttempts) { // Try to preview const result = await preview(currentCode); if (!result.error) { // Success! return { success: true }; } // Preview failed - attempt repair attempts++; console.warn(`[Auto-Repair] Attempt ${attempts}/${maxAttempts}`); const repaired = await gptCodeRepair(currentCode, result.error); if (repaired) { currentCode = repaired; // Update variant in state updateVariantCode(variantId, repaired); } else { // Repair failed break; } } // All attempts failed return { success: false, error: 'Auto-repair failed after 3 attempts' }; } ``` **GPT Repair Prompt**: ``` You are a code repair assistant. Fix this syntax error in the JSX code: Error: Unexpected token '<' Code: {variantCode} Return only the fixed code, no explanations. ``` **Success Rate**: ~85% of syntax errors are fixed automatically. *** ## Comparison with Alternatives ### Alternative 1: Git Branches (Rejected) **Approach**: Each variant on a separate branch. ``` main ├── variant-hero-cta-1 ├── variant-hero-cta-2 ├── variant-pricing-1 └── variant-pricing-2 ``` **Problems**: - ❌ **Branch explosion** - 10 elements × 3 variants = 30 branches - ❌ **Merge conflicts** - Switching variants requires merge - ❌ **Slow** - Branch checkout takes seconds - ❌ **Complex** - Git operations visible to user ### Alternative 2: Two Files (Rejected) **Approach**: `hero.tsx` and `hero-variant.tsx` **Problems**: - ❌ **Import duplication** - Both files need same imports - ❌ **Maintenance burden** - Update both files for refactors - ❌ **Component coordination** - Which file is "active"? - ❌ **File clutter** - Project polluted with variant files ### Alternative 3: Continuous Diff Tracking (Rejected) **Approach**: File watchers diff changes in real-time. **Problems**: - ❌ **Complex** - Diff algorithm, merge logic, conflict resolution - ❌ **Resource-intensive** - Continuous file watching - ❌ **Race conditions** - User edits while swap in progress - ❌ **Unreliable** - File watchers can miss changes ### Our Approach: Just-in-Time Swap (Chosen) **Advantages**: - ✅ **Simple** - Read → Capture → Write - ✅ **Reliable** - No race conditions, no missed edits - ✅ **Performant** - Only work on user action - ✅ **One file** - No duplication, no clutter - ✅ **Stateless** - activePreview is only state needed *** ## Performance Optimization ### 1. Boundary Caching **Problem**: `findElementBoundaries()` is expensive (~50ms on large files). **Solution**: Cache boundaries after first lookup. ```typescript function applyPreview(...) { const boundaries = snapshot.boundaries || findElementBoundaries(lines, keakId); // Cache for next swap snapshot.boundaries = boundaries; } ``` **Benefit**: Subsequent swaps skip boundary detection entirely. ### 2. In-Memory Content Cache **Problem**: Reading files from disk is slow (~10-20ms). **Solution**: Cache file content in memory. ```typescript class FileStateManager { private currentContent = new Map(); applyPreview(filePath, keakId, fileContent, variantCode) { // ... swap logic ... // Cache the new content this.currentContent.set(filePath, newContent); } } ``` **Benefit**: Rapid swaps (preview → hide → preview) use cached content. **Trade-off**: Cache can become stale if file is edited externally. That's why we always read fresh from disk at swap time. ### 3. Debounced Persistence **Problem**: Saving snapshot on every edit → excessive disk writes. **Solution**: 500ms debounce (see auto-save effect above). **Benefit**: 10 rapid edits → 1 disk write. *** ## Testing the Preview System ### Manual Test Cases **Test 1: Basic Preview/Hide**: 1. Select element 2. Generate variant 3. Click "Preview" → Variant shows in browser 4. Click "Hide Preview" → Control shows in browser 5. Click "Preview" again → Variant shows again **Expected**: Both versions preserved across swaps. **Test 2: Edit Control**: 1. Select element (control on disk) 2. Edit control in Monaco 3. Save (Cmd+S) 4. Click "Preview" **Expected**: Control edit captured in snapshot.originalCode. **Test 3: Edit Variant**: 1. Preview variant 2. Edit variant in VS Code 3. Click "Hide Preview" **Expected**: Variant edit captured in snapshot.variantCode. **Test 4: App Restart**: 1. Preview variant 2. Close IDE 3. Reopen IDE 4. Select same element 5. Click "Hide Preview" **Expected**: Both versions intact. *** ## Related Documentation - **[Core Concepts](./core-concepts)** - Snapshot system, control vs variant concept - **[State Management](./state-management)** - How snapshots are persisted - **[Element Tracking](./element-tracking)** - How boundaries are detected - **[Architecture](./architecture)** - Service layer and hooks --- /docs/extension-ide/ide/setup # Setup & Installation Get Keak-Code IDE running on your project in 5 minutes. *** ## Prerequisites ### System Requirements - **Node.js**: 16.x or higher - **npm** or **yarn**: Latest version - **Operating System**: macOS, Windows, or Linux - **Git**: Optional but recommended ### Supported Frameworks Keak-Code works with: - ✅ **Next.js** (App Router or Pages Router) - ✅ **Create React App** - ✅ **Vite** - ✅ **Static HTML** (with build step) **Key requirement**: Project must have a dev server that supports hot module replacement (HMR). *** ## Installation ### Download Keak-Code **Official website**: [keak.com/download](https://keak.com/download) **Options**: - macOS: `.dmg` installer - Windows: `.exe` installer - Linux: `.AppImage` or `.deb` ### First Launch **Step 1**: Open Keak-Code - macOS: Applications folder - Windows: Start menu - Linux: Application launcher **Step 2**: Login to Keak - Enter email and password - Or use "Sign in with Google" **Step 3**: You'll see WelcomeScreen with two options: - **Import from GitHub** (OAuth flow) - **Open Local Folder** (browse file system) *** ## Project Setup ### Option 1: Import from GitHub **Use case**: Clone an existing repository. **Steps**: 1. Click "Import from GitHub" 2. Authorize Keak to access your repositories 3. Select repository from list 4. Choose local directory for clone 5. Wait for clone to complete (~10-60s depending on repo size) 6. Project opens automatically **Result**: Repository cloned to `~/keak-projects/{repo-name}`. ### Option 2: Open Local Folder **Use case**: Work with existing local project. **Steps**: 1. Click "Open Local Folder" 2. Browse to project directory 3. Select folder 4. Keak-Code opens project **Result**: Project loaded, dev server will be configured next. *** ## Automatic SDK Setup ### What Happens When you open a project, Keak-Code **automatically**: 1. **Detects your project type** (React, Next.js, Vite, CRA, etc.) 2. **Installs @keak/sdk** if not already present (~10 seconds) 3. **Adds KeakToolbar** to your app layout (for element selection) 4. **Configures Babel plugin** (or Webpack loader) for source mapping 5. **Sets up dev server** configuration 6. **Creates .keak/** directory for project data **You'll see** setup progress in the **top toolbar**: ``` Setting up Keak for your project... ✓ Detected project type: Next.js (App Router) ✓ Installing @keak/sdk... ✓ Configuring KeakToolbar... ✓ Configured Babel plugin ✓ Dev server ready ``` **Time**: ~10-30 seconds (depends on npm install speed). **What gets installed**: - `@keak/sdk` package added to dependencies - Babel plugin configured in `babel.config.js` - KeakToolbar component injected (automatically) - `.keak/` directory created (for project config and history) ### Manual Setup (If Automatic Fails) **Open Setup Panel** (right sidebar → Setup tab): ``` 1. Install SDK npm install @keak/sdk [ Install ] button 2. Configure Babel Plugin [ Auto-Configure ] button 3. Add KeakToolbar [ Add Toolbar ] button 4. Start Dev Server [ Start ] button ``` **Each step** can be run individually if automatic setup fails. *** ## SDK Installation ### What is @keak/sdk? The Keak SDK provides: - `` component for A/B testing - `` component for control/treatment - Event tracking utilities - Variant assignment logic ### Installation Command ```bash npm install @keak/sdk # or yarn add @keak/sdk ``` **Package size**: ~50KB (minified + gzipped). **Dependencies**: None (peer dependencies: react, react-dom). ### Verification **Check package.json**: ```json { "dependencies": { "@keak/sdk": "^1.0.0" } } ``` **Check node\_modules**: ```bash ls node_modules/@keak/sdk # Should show: package.json, dist/, README.md ``` *** ## Babel Plugin Configuration ### Automatic Configuration **What Keak-Code does**: 1. Detects babel config file: - `babel.config.js` - `.babelrc` - `.babelrc.json` - `package.json` (babel field) 2. Injects plugin: ```javascript ['@keak/babel-plugin-keak', { enabled: process.env.NODE_ENV === 'development', verbose: true }] ``` 3. Restarts dev server (if running) ### Manual Configuration **If automatic fails**, add manually: **babel.config.js**: ```javascript module.exports = { presets: [ // ... your existing presets ], plugins: [ // ... your existing plugins ['@keak/babel-plugin-keak', { enabled: process.env.NODE_ENV === 'development', verbose: true }] ] }; ``` **Vite** (vite.config.ts): ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [ react({ babel: { plugins: [ ['@keak/babel-plugin-keak', { enabled: true }] ] } }) ] }); ``` **Restart dev server** after configuration. ### Verification **Check build logs** (Terminal tab): ``` [Keak Babel Plugin] Starting... [Keak Babel Plugin] Processing src/components/hero.tsx... [Keak Babel Plugin] Tagged 8 elements in hero.tsx [Keak Babel Plugin] ✓ Tagged 157 elements across 24 files in 342ms ``` **Check rendered HTML** (inspect element in preview): ```html ``` If you see `data-keak-src`, **plugin is working** ✅. *** ## KeakToolbar Installation ### What is KeakToolbar? The **KeakToolbar** is a React component that gets injected into your app to enable: - Visual element selection (click any element to optimize it) - Selection mode toggle ("Select Element" button) - Element hover effects (blue outlines) - Element click handling (green pulsing outline, opens CRO drawer) **Location in preview**: Blue "K" icon in bottom-right corner. ### Automatic Installation Keak-Code **automatically adds** KeakToolbar to your app layout during setup. **For Next.js App Router**, it injects into: - `app/layout.tsx` (root layout) - Wraps `{children}` with `` **Example injection**: ```typescript // Before export default function RootLayout({ children }) { return ( {children} ); } // After import { KeakProvider } from '@keak/sdk'; export default function RootLayout({ children }) { return ( {children} ); } ``` **For Next.js Pages Router**, it injects into: - `pages/_app.tsx` **For Create React App / Vite**, it injects into: - `src/App.tsx` or `src/main.tsx` ### Manual Installation **If automatic injection fails**, add manually: ```tsx import { KeakProvider } from '@keak/sdk'; function App() { return ( {/* Your app components */} ); } ``` **KeakProvider** automatically: - Renders the toolbar (blue "K" icon) - Sets up WebSocket connection to IDE - Handles element selection events - Shows/hides toolbar based on environment ### Verification **In preview browser**: 1. Look for blue "K" icon (bottom-right corner) 2. Click it → Toolbar expands 3. Should show: - Keak logo - "Select Element" button - Close button (×) **If toolbar missing**: - Check Setup Panel → "KeakToolbar Status" - Should show "✅ Installed in app/layout.tsx" - If not, click "Add Toolbar" button - Restart dev server **Inspect toolbar element**: ```html
``` **The toolbar is only visible in development** (process.env.NODE\_ENV === 'development'). *** ## Dev Server Configuration ### Auto-Detection Keak-Code detects dev server command from: | Framework | Command | Port | |-----------|---------|------| | Next.js | `npm run dev` | 3000 | | Vite | `npm run dev` | 5173 | | CRA | `npm start` | 3000 | | Custom | (from package.json scripts) | (varies) | **Script detection** (from package.json): ```json { "scripts": { "dev": "next dev", // ← Keak uses this "start": "react-scripts start", "serve": "vite" } } ``` ### Manual Configuration **If auto-detection fails**, configure manually in Setup Panel: ``` Dev Server Configuration ├─ Command: npm run dev ├─ Port: 3000 └─ [ Save & Start ] button ``` **Saved to**: `.keak/project.json` ```json { "projectId": "uuid-123", "projectName": "My SaaS App", "devServer": { "command": "npm run dev", "port": 3000 } } ``` *** ## Domain Linking ### Why Link a Domain? **Required for**: Saving experiments to Dashboard. **Without domain linking**: - ❌ Can generate variants (works locally) - ❌ Can preview variants (works locally) - ❌ **Cannot save experiments** to Dashboard **With domain linking**: - ✅ All local features work - ✅ Experiments sync to Dashboard - ✅ Analytics tracked - ✅ Team collaboration enabled ### How to Link **Step 1**: Open project in Keak-Code **Step 2**: Open Dashboard (web browser) - Navigate to Projects tab - Find your project - Click "Link Domain" **Step 3**: Select domain from dropdown - Domains must be added first (Dashboard → Domains → Add Domain) **Step 4**: Return to Keak-Code - IDE auto-refreshes on window focus - Detects domain linking - Shows confirmation: "Domain linked: example.com" **Verification**: ```typescript // In IDE console const project = useDesktopProject.getState().currentProject; console.log(project.domain_id); // Should be a UUID, not null ``` *** ## Quick Start Guide Once setup completes, follow these steps to create your first variant: ### Step 1: Start Dev Server **Click "Start Dev"** button (top toolbar) to run your project. **You'll see**: ``` > my-app@1.0.0 dev > next dev - ready started server on 0.0.0.0:3000 ✓ Compiled successfully ``` **Wait for**: "✓ Compiled successfully" message. ### Step 2: Switch to Preview **Click "Preview" tab** (center panel) to see your app running. **Look for**: Blue "K" icon in bottom-right corner of preview. **If missing**: - Refresh preview (Cmd+R or Ctrl+R) - Check Terminal for errors - Verify dev server is running ### Step 3: Open CRO Toolbar **Click the blue "K" icon** (bottom-right) → Toolbar expands. **You'll see**: - "Select Element" button - Keak logo and branding - Toolbar UI overlaying your app ### Step 4: Select Element **Click "Select Element"** button → Enter selection mode. **You'll notice**: - Cursor changes to crosshair - Hovering over elements shows **blue outline** - Element details displayed on hover **Click an element** (button, heading, etc.) → **Green pulsing outline** appears. **Result**: CRO drawer opens on right side with: - Element details - AI-suggested prompts - Input area for custom goal ### Step 5: Generate Variant **Option A**: Click an AI-suggested prompt chip **Option B**: Enter your own goal (e.g., "Make this more compelling") **Click "Generate"** button. **You'll see**: - Streaming progress updates (router → optimizer → validator) - Variant appears with CRO analysis - Variant auto-previewed in browser **Time**: ~20-30 seconds from prompt to variant. ### Step 6: Preview & Apply **Toggle preview**: - Click "Preview" → See variant in browser - Click "Hide Preview" → See control in browser **Edit variant**: Open code in Monaco editor, make changes, save. **When satisfied**: Click "Apply Variant" → Experiment wrapper added to code. **Result**: A/B test is live locally! Deploy to production via git. *** ## Verification Checklist After setup, verify everything works: ### 1. Dev Server Running **Check Terminal tab**: ``` > my-app@1.0.0 dev > next dev - ready started server on 0.0.0.0:3000 ✓ Compiled successfully ``` **Check Preview panel**: - Shows your app - **Blue "K" icon** visible in bottom-right corner - Can navigate pages ### 2. CRO Toolbar Accessible **Test**: 1. Switch to Preview mode 2. Look for blue "K" icon (bottom-right) 3. Click it → Toolbar should expand **If fails**: - Check KeakToolbar was injected (Setup Panel) - Verify dev server is running - Check build logs for errors ### 3. Element Selection Works **Test**: 1. Click "Select Element" in toolbar 2. Hover over any element (button, heading, etc.) 3. Blue outline should appear 4. Click element → CRO drawer opens on right side **If fails**: - Check WebSocket connection (look for errors in Terminal) - Verify Babel plugin is working - Check that data-keak-src attributes exist (inspect element) ### 3. Babel Plugin Working **Check**: 1. Inspect element in preview (right-click → Inspect) 2. Look for `data-keak-src` attribute **Example**: ```html ``` **If missing**: - Check babel.config.js has Keak plugin - Restart dev server - Check build logs for errors **Build logs should show**: ``` [Keak Babel Plugin] Starting... [Keak Babel Plugin] Processing src/components/hero.tsx... [Keak Babel Plugin] Tagged 8 elements in hero.tsx [Keak Babel Plugin] ✓ Tagged 157 elements across 24 files in 342ms ``` ### 4. KeakToolbar Injected **Check**: 1. Open preview in browser 2. Look for blue "K" icon in bottom-right corner 3. Click it → Toolbar should expand with "Select Element" button **If missing**: - Check Setup Panel → "KeakToolbar Status" - Re-run "Add KeakToolbar" button - Refresh preview (Cmd+R) **Toolbar should show**: - Keak logo - "Select Element" button - Collapsible panel ### 5. SDK Installed **Check package.json**: ```bash cat package.json | grep "@keak/sdk" # Should show: "@keak/sdk": "^1.0.0" ``` **Check imports** (in any file): ```typescript import { Experiment, Variant } from '@keak/sdk'; // Should not error (TypeScript finds types) ``` **Check node\_modules**: ```bash ls node_modules/@keak/sdk # Should show: package.json, dist/, README.md, src/ ``` ### 6. Domain Linked **Check** (if you plan to save experiments): 1. Open Setup panel 2. "Domain Status" should show: "✅ Linked to example.com" **If not linked**: - Go to Dashboard (web) - Link domain to this project - Return to IDE (auto-refreshes) *** ## Troubleshooting Common Issues ### Issue: Dev Server Won't Start **Symptoms**: - Terminal shows error - Preview panel says "Waiting for dev server..." **Checks**: 1. **Port already in use**: ```bash lsof -i :3000 # macOS/Linux netstat -ano | findstr :3000 # Windows ``` **Fix**: Kill process or use different port 2. **Missing dependencies**: ```bash npm install # Reinstall dependencies ``` 3. **Incorrect command**: - Check package.json scripts - Verify command in Setup panel ### Issue: Babel Plugin Not Injecting **Symptoms**: - No `data-keak-src` in rendered HTML - Element selection is slow (file search fallback) **Checks**: 1. **Plugin installed**: ```bash npm list @keak/babel-plugin-keak ``` 2. **babel.config.js exists and has plugin** 3. **NODE\_ENV is development**: ```bash echo $NODE_ENV # Should be 'development' ``` **Fix**: - Re-run Setup Panel → "Configure Babel Plugin" - Restart dev server - Check build logs for plugin errors ### Issue: Authentication Failed **Symptoms**: - Login screen loops - "Invalid credentials" error **Checks**: 1. **Correct email/password** 2. **Dashboard API reachable**: ```bash curl https://dashboard.keak.com/api/health ``` 3. **Internet connection** **Fix**: - Try "Forgot Password" - Contact support if persistent ### Issue: Domain Not Linking **Symptoms**: - Domain linked in Dashboard - IDE still shows "No domain linked" **Fix**: 1. **Click IDE window** (triggers focus event → auto-refresh) 2. **Manually refresh**: - Setup Panel → "Refresh Project" button 3. **Check project UUID**: - Dashboard → Projects → Copy project ID - IDE → Should match in `.keak/project.json` *** *** ## Development Setup (For Contributors) **For users**: Skip this section. The sections above are for using Keak-Code. **For contributors**: Follow these steps to work on Keak-Code itself. ### Clone Repository ```bash git clone https://github.com/keak/keak-code.git cd keak-code ``` ### Install Dependencies ```bash npm install ``` **Installs**: - Electron + build tools - React + TypeScript - Vite (for renderer process) - All dependencies (~500MB in node\_modules) ### Run in Development ```bash npm run dev ``` **This starts**: - **Vite dev server** (port 5173) - Renderer process with HMR - **Electron app** - Main process connected to Vite **Hot reload works**: - Edit `src/renderer/src/` files → Instant reload - Edit `src/main/` files → Electron restarts (slower) ### Build for Distribution **macOS**: ```bash npm run build:mac ``` **Outputs**: `release/Keak-Code.dmg` **Windows**: ```bash npm run build:win ``` **Outputs**: `release/Keak-Code Setup.exe` (NSIS installer) **Linux**: ```bash npm run build:linux ``` **Outputs**: `release/Keak-Code.AppImage` ### Project Structure (Electron) ``` keak-code/ ├── src/ │ ├── main/ # Electron main process (Node.js) │ │ ├── index.ts # Entry point │ │ └── ipc/ # IPC handlers (file system, git, dev server) │ └── renderer/ # Renderer process (React) │ └── src/ │ ├── App.tsx # Main application │ ├── components/ # React components │ ├── hooks/ # Custom hooks │ ├── services/ # Business logic │ ├── state/ # Zustand stores │ └── utils/ # Utilities ├── electron.vite.config.ts # Vite config for Electron ├── package.json └── release/ # Build output (gitignored) ``` ### Security Model **Keak-Code uses Electron security best practices**: ```typescript // src/main/index.ts const mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, '../preload/index.js'), contextIsolation: true, // ✅ Isolate renderer from main nodeIntegration: false, // ✅ No Node.js in renderer sandbox: true // ✅ Sandbox renderer process } }); ``` **IPC Handlers** validate all inputs: ```typescript // src/main/ipc/file-handlers.ts ipcMain.handle('read-file', async (event, filePath: string) => { // Validate filePath is within project root if (!isWithinProjectRoot(filePath, projectRoot)) { throw new Error('Access denied: Path outside project root'); } return fs.readFileSync(filePath, 'utf-8'); }); ``` **Webviews are sandboxed**: ```typescript ``` See [Architecture](./architecture.md#security-architecture) for more details. *** ## Next Steps After successful setup: 1. **Read** [Core Concepts](./core-concepts) to understand key concepts 2. **Try** the Quick Start Guide above to create your first variant 3. **Explore** [Experiment Workflow](./experiment-workflow) for complete guide 4. **Dive into** [Architecture](./architecture) for technical details *** ## Related Documentation - **[Architecture](./architecture)** - Technical deep dive - **[Element Tracking](./element-tracking)** - How KeakToolbar selects elements - **[Babel Integration](./babel-integration)** - Plugin details - **[Troubleshooting](./troubleshooting)** - Common issues and fixes --- /docs/extension-ide/ide/state-management # State Management **The Challenge**: How do you keep conversation history, variant data, and preview state synchronized across app restarts, tab switches, and drawer closures? **The Solution**: A 3-tier state architecture with strategic persistence at each level. *** ## Why Three Tiers? **The fundamental problem**: Different parts of state have different lifecycles. **Examples**: - **Chat tabs** (openTabs) → Should survive app restart - **Generation progress** (isGenerating) → Should NOT survive (only relevant during active generation) - **Conversation messages** → Should survive everything, even if drawer closed - **FileStateManager snapshots** → Must be perfectly in sync with conversations **One-tier solution** (everything in React state): - ❌ Lost on component unmount - ❌ Lost on app restart - ❌ No cross-tab persistence **Two-tier solution** (React + localStorage): - ❌ localStorage limited to 5-10MB - ❌ No project-specific isolation - ❌ Cleared when user clears browser data **Three-tier solution** ✅: - Tier 1 (Zustand + localStorage): UI state, survives app restart - Tier 2 (ChatPersistence + file system): Conversation history, survives everything - Tier 3 (React state): Transient UI state, lost on unmount (intentional) Each tier serves a specific purpose. *** ## 3-Tier Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ Tier 1: Zustand Store (Global UI State) │ │ Location: state/chatStore.ts │ │ Persistence: localStorage (partial) │ │ Scope: App-wide │ │ Lifecycle: Survives app restart, lost on localStorage clear│ │ │ │ Stores: │ │ ├─ elements: Record (persisted) │ │ ├─ chats: Record (persisted) │ │ ├─ openTabs: string[] (persisted) │ │ └─ Runtime: activeKeakId, activeView, isGenerating │ └─────────────────────────────────────────────────────────────┘ ▼ Auto-sync on state change ┌─────────────────────────────────────────────────────────────┐ │ Tier 2: ChatPersistence (Project History) │ │ Location: services/chatPersistence.ts │ │ Persistence: .keak/history/history.json │ │ Scope: Per-project │ │ Lifecycle: Survives everything (file-based) │ │ │ │ Stores: │ │ └─ Complete conversation state per keakId: │ │ ├─ messages, variants, elementSnapshot │ │ ├─ userGoal, promptCount, validationResults │ │ ├─ previewVariantId, editedVariantCode │ │ └─ chatId, chatLastVariantId (telemetry linking) │ └─────────────────────────────────────────────────────────────┘ ▲ 500ms debounced auto-save ┌─────────────────────────────────────────────────────────────┐ │ Tier 3: React Component State (Transient) │ │ Location: hooks/use-cro-state.ts │ │ Persistence: None (ephemeral) │ │ Scope: Active CRO session (single element) │ │ Lifecycle: Lost on component unmount │ │ │ │ Stores: │ │ └─ Component-level state for current element: │ │ ├─ variants, messages, userGoal │ │ ├─ previewVariantId, editedVariantCode │ │ ├─ isGenerating, sessionId, chatId │ │ └─ conversationHistory (multi-element tracking) │ └─────────────────────────────────────────────────────────────┘ ``` *** ## Tier 1: Zustand Store (Global UI State) **Location**: `state/chatStore.ts` **Purpose**: Global UI state and multi-tab management. ### Store Definition ```typescript interface ChatStore { // ===== Persisted State (localStorage) ===== elements: Record; // Element metadata by keakId chats: Record; // Chat metadata by keakId openTabs: string[]; // Array of keakIds for open tabs // ===== Runtime State (ephemeral) ===== activeKeakId: string | null; // Currently selected tab activeView: 'chat' | 'editor' | 'history'; // CRO drawer view mode isGenerating: boolean; // Generation in progress? generationPhase: 'idle' | 'reasoning' | 'applying-edits' | 'complete'; thinkingMessages: string[]; // Streaming progress messages activePreviews: Record; // Preview state per element // ===== Actions ===== registerElement: (data: RegisterElementData) => void; selectElement: (keakId: string) => void; initChat: (keakId: string) => void; setGenerating: (isGenerating: boolean, phase?: string) => void; addThinkingMessage: (message: string) => void; clearThinkingMessages: () => void; // ... more actions } ``` ### Interface Definitions ```typescript interface ElementData { keakId: string; filePath: string; elementCode: string; // outerHTML at selection time originalCode?: string; // Original code from FileStateManager tagName?: string; textContent?: string; } interface ChatData { keakId: string; initialMessage: Message; // First assistant message createdAt: number; // Timestamp } interface ActivePreview { keakId: string; variantId: string; previewActive: boolean; } ``` ### Persistence Middleware ```typescript export const useKeakStore = create()( persist( (set, get) => ({ // Initial state elements: {}, chats: {}, openTabs: [], activeKeakId: null, activeView: 'chat', isGenerating: false, // ... default values // Actions implementation registerElement: (data) => {/* ... */}, selectElement: (keakId) => {/* ... */}, // ... }), { name: 'keak-chat-store', // localStorage key storage: createJSONStorage(() => localStorage), partialize: (state) => ({ // Only persist these fields elements: state.elements, chats: state.chats, openTabs: state.openTabs, }), } ) ); ``` **Key insight**: `partialize` selects **only** the fields to persist. Runtime state (isGenerating, thinkingMessages) is intentionally ephemeral. ### Key Actions #### registerElement ```typescript registerElement: (data: RegisterElementData) => { set((state) => ({ elements: { ...state.elements, [data.keakId]: { keakId: data.keakId, filePath: data.filePath, elementCode: data.elementCode, originalCode: data.originalCode, tagName: data.tagName, textContent: data.textContent, } } })); } ``` **When called**: Element selected in preview browser. **Purpose**: Store element metadata for quick lookup. #### selectElement ```typescript selectElement: (keakId: string) => { set((state) => { // Add to openTabs if not already there const newTabs = state.openTabs.includes(keakId) ? state.openTabs : [...state.openTabs, keakId]; return { activeKeakId: keakId, openTabs: newTabs, activeView: 'chat' // Switch to chat view when element selected }; }); } ``` **When called**: Element selected, or user clicks a tab. **Purpose**: Set active chat and ensure tab is open. **Result**: CRODrawer re-renders with new activeKeakId, triggering chat hydration. *** ## Tier 2: ChatPersistence (Project History) **Location**: `services/chatPersistence.ts` **Purpose**: Project-specific conversation history with file-based persistence. **Why file-based?** - ✅ **Unlimited storage** (not limited to 5-10MB like localStorage) - ✅ **Project-specific** (isolated per project, not global) - ✅ **Git-aware** (can be gitignored, doesn't pollute commits) - ✅ **Survives browser clear** (not affected by localStorage wipes) ### Storage Location **Path**: `.keak/history/history.json` (in project root) **Why `.keak/`?** - Hidden directory (starts with `.`) - Conventional (like `.git`, `.vscode`) - Easy to add to `.gitignore` ### Data Structure ```typescript interface PersistedChatState { // ===== Element Metadata ===== element: { keakId: string; outerHTML: string; innerHTML: string; className: string; tagName: string; textContent: string; }; // ===== Source Tracking ===== sourceCode: string; // Full page JSX source sourceFilePath: string; // Element's file path projectRoot: string; // Project root directory // ===== Chat State ===== chatId: string | null; // Telemetry chat ID chatLastVariantId: string | null; // Last variant ID (for multi-turn) messages: Message[]; // Conversation history userGoal: string; // User's current goal promptCount: number; // Number of prompts sent // ===== Variants ===== variants: CROVariant[]; // All generated variants validationResults: ValidationResult[]; // Code quality issues previewVariantId: string | null; // Currently previewed variant editedVariantCode: Record; // variantId → edited code // ===== Feedback ===== variantFeedback: Record; // ===== FileStateManager Snapshot ===== originalSourceCode: string | null; hasUnsavedChanges: boolean; elementSnapshot: { controlCode: string; variantCode: string | null; activePreview: boolean; } | null; // ===== Conversation History (Multi-Element) ===== conversationHistory: Map; // ===== Session Tracking ===== selectedModel: string; // 'turbo', 'pro', 'max' timestamp: number; // Last update time } ``` **File format**: ```typescript interface HistoryFile { chats: Record; // keakId → complete state } ``` **Example** (`.keak/history/history.json`): ```json { "chats": { "hero-cta-button": { "element": { "keakId": "hero-cta-button", "outerHTML": "", "tagName": "button", "textContent": "Sign Up" }, "sourceFilePath": "components/hero.tsx", "messages": [ { "role": "user", "content": "Make it more compelling" }, { "role": "assistant", "content": "Analysis...", "variant": {...} } ], "variants": [ { "id": "v1", "code": "", "analysis": "..." } ], "elementSnapshot": { "controlCode": "", "variantCode": "", "activePreview": true }, "previewVariantId": "v1", "chatId": "uuid-123", "timestamp": 1706123456789 }, "pricing-cta": { // Different element, different conversation } } } ``` ### Key Methods ```typescript class ChatPersistence { private historyPath: string; // .keak/history/history.json // Load conversation for specific element async loadChat(keakId: string): Promise { const history = await this.readHistory(); return history.chats[keakId] || null; } // Save conversation (called by auto-save hook) async saveChat(keakId: string, state: PersistedChatState): Promise { const history = await this.readHistory(); history.chats[keakId] = { ...state, timestamp: Date.now() }; await this.writeHistory(history); } // Load all conversations (for history panel) async loadHistory(): Promise { try { const content = await electronAPI.readFile(this.historyPath); return JSON.parse(content); } catch { return { chats: {} }; // File doesn't exist yet } } // Delete conversation async clearChat(keakId: string): Promise { const history = await this.readHistory(); delete history.chats[keakId]; await this.writeHistory(history); // Also clear from telemetry await telemetry.clearChat(chatId); } // Internal: Write to disk private async writeHistory(history: HistoryFile): Promise { await electronAPI.ensureDir(path.dirname(this.historyPath)); await electronAPI.writeFile( this.historyPath, JSON.stringify(history, null, 2) // Pretty-print for debugging ); } } ``` *** ## Tier 3: React Component State (Transient) **Location**: `hooks/use-cro-state.ts` **Purpose**: Active session state for the currently open element. **Why separate from Zustand?** - **Component-scoped** - Only relevant while CRODrawer is mounted - **Transient** - Lost on drawer close (intentional) - **Auto-saves** to ChatPersistence (bridging to Tier 2) ### State Variables ```typescript function useCROState(selectedElement, sourceCode) { // ===== Generation State ===== const [userGoal, setUserGoal] = useState(''); const [variants, setVariants] = useState([]); const [isGenerating, setIsGenerating] = useState(false); // ===== Preview State ===== const [previewVariantId, setPreviewVariantId] = useState(null); const [editedVariantCode, setEditedVariantCode] = useState(new Map()); const [originalSourceCode, setOriginalSourceCode] = useState(null); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // ===== Conversation State ===== const [messages, setMessages] = useState([]); const [conversationHistory, setConversationHistory] = useState(new Map()); // ===== Validation State ===== const [validationResults, setValidationResults] = useState([]); const [complexityScore, setComplexityScore] = useState(null); // ===== Telemetry State ===== const [sessionId, setSessionId] = useState(null); const [chatId, setChatId] = useState(null); const [chatLastVariantId, setChatLastVariantId] = useState(null); // ===== UI State ===== const [uploadedImages, setUploadedImages] = useState([]); const [selectedModel, setSelectedModel] = useState('turbo'); const [applyingVariantId, setApplyingVariantId] = useState(null); // ... (see auto-save effect below) } ``` **Total**: ~15 state variables, all scoped to the active element. ### Auto-Save Effect (500ms Debounce) **The bridge** between Tier 3 (React state) and Tier 2 (ChatPersistence): ```typescript useEffect(() => { // Debounce saves to avoid excessive disk writes const timeoutId = setTimeout(() => { if (!selectedElement?.keakId) return; // Get latest snapshot from FileStateManager const fileManager = getFileStateManager(); const snapshot = fileManager.getSnapshot(selectedElement.keakId); // Save complete state to ChatPersistence persistence.saveChat(selectedElement.keakId, { // Element metadata element: { keakId: selectedElement.keakId, outerHTML: selectedElement.outerHTML || '', innerHTML: selectedElement.innerHTML || '', className: selectedElement.className || '', tagName: selectedElement.tagName || '', textContent: selectedElement.textContent || '' }, // Source tracking sourceCode, sourceFilePath: selectedElement.sourceFilePath || '', projectRoot: root || '', // Chat state chatId, chatLastVariantId, messages, userGoal, promptCount: variants.length, // Variants variants, validationResults, previewVariantId, editedVariantCode: Object.fromEntries(editedVariantCode), // Feedback variantFeedback: {}, // TODO: Implement feedback tracking // FileStateManager snapshot originalSourceCode, hasUnsavedChanges, elementSnapshot: snapshot ? { controlCode: snapshot.originalCode, variantCode: snapshot.variantCode, activePreview: snapshot.activePreview } : null, // Conversation history conversationHistory, // Session tracking selectedModel, timestamp: Date.now() }); }, 500); // 500ms debounce return () => clearTimeout(timeoutId); }, [ // Re-run when ANY of these change selectedElement, messages, variants, userGoal, previewVariantId, editedVariantCode, validationResults, originalSourceCode, hasUnsavedChanges, chatId, chatLastVariantId, conversationHistory, selectedModel ]); ``` **Behavior**: - User makes 10 rapid changes → Only 1 save (500ms after last change) - User switches tabs → Save triggered immediately (cleanup runs) - App crashes → Max 500ms of data loss **Why 500ms?** - Fast enough to feel instant - Slow enough to batch rapid changes - Sweet spot for disk I/O *** ## Data Flow: Element Selection → Chat Opens **The story**: User clicks element, chat opens with full history. ### Sequence ```typescript // ===== Step 1: Element selected in preview ===== // App.tsx receives WebSocket message function handleWebSocketElementSelected(elementData) { const keakId = extractOrGenerateKeakId(elementData); const filePath = await resolveElementSource(elementData); // ===== Step 2: Register in Zustand ===== useKeakStore.getState().registerElement({ keakId, filePath, elementCode: elementData.outerHTML, tagName: elementData.tagName, textContent: elementData.textContent }); // ===== Step 3: Select element (opens tab) ===== useKeakStore.getState().selectElement(keakId); // This sets activeKeakId, which triggers CRODrawer re-render } ``` ### CRODrawer Hydration ```typescript // ===== Step 4: CRODrawer detects activeKeakId change ===== // hooks/use-conversation-history.ts useEffect(() => { if (!activeKeakId) return; const loadConversation = async () => { // ===== Step 5: Load from ChatPersistence ===== const persistedChat = await chatPersistence.loadChat(activeKeakId); if (persistedChat) { // ===== Step 6: Restore FileStateManager snapshot FIRST ===== if (persistedChat.elementSnapshot) { fileStateManager.restoreSnapshot( activeKeakId, persistedChat.sourceFilePath, persistedChat.elementSnapshot ); } // ===== Step 7: Hydrate React state ===== setMessages(persistedChat.messages); setVariants(persistedChat.variants); setUserGoal(persistedChat.userGoal); setPreviewVariantId(persistedChat.previewVariantId); setEditedVariantCode(new Map(Object.entries(persistedChat.editedVariantCode))); setValidationResults(persistedChat.validationResults); setChatId(persistedChat.chatId); setChatLastVariantId(persistedChat.chatLastVariantId); setConversationHistory(persistedChat.conversationHistory); setOriginalSourceCode(persistedChat.originalSourceCode); setHasUnsavedChanges(persistedChat.hasUnsavedChanges); setSelectedModel(persistedChat.selectedModel); // Done! Full state restored. } else { // ===== Step 8: New chat - initialize with greeting ===== setMessages([{ role: 'assistant', content: 'Tell me what you'd like to improve about this element.' }]); setVariants([]); setUserGoal(''); // ... reset all state } }; loadConversation(); }, [activeKeakId]); // Re-run when active element changes ``` **Time**: ~10-50ms for hydration (depends on conversation size). **Result**: User sees full conversation history instantly. *** ## Data Flow: Variant Generation → State Updates **The story**: User generates a variant, state flows through all three tiers. ### Sequence ```typescript // ===== Step 1: User submits prompt ===== const handleGenerate = async (goal: string) => { // ===== Step 2: Update Tier 3 (React state) ===== setIsGenerating(true); setUserGoal(goal); const newMessages = [...messages, { role: 'user', content: goal }]; setMessages(newMessages); // ===== Step 3: Update Tier 1 (Zustand) ===== useKeakStore.getState().setGenerating(true, 'reasoning'); try { // ===== Step 4: Call KeaFab API ===== const result = await generator.generate(goal, { selectedComponent, sourceCode, screenshot, chatLastVariantId, }); // ===== Step 5: Update Tier 3 with result ===== const assistantMessage = { role: 'assistant', content: result.analysis, variant: result.variant }; setMessages([...newMessages, assistantMessage]); setVariants([...variants, result.variant]); // ===== Step 6: Update conversation history (multi-element tracking) ===== const elementId = `${selectedComponent.tagName}-${selectedComponent.textContent}`; conversationHistory.set(elementId, { originalSource: originalSourceCode, messages: [...newMessages, assistantMessage], variants: [...variants, result.variant] }); setConversationHistory(new Map(conversationHistory)); // ===== Step 7: Auto-preview (triggers FileStateManager) ===== await applier.previewWithRepair(result.variant.code); setPreviewVariantId(result.variant.id); // ===== Step 8: Store telemetry IDs ===== setChatLastVariantId(result.variantId); // ===== Step 9: Tier 3 auto-save triggers (500ms debounce) ===== // useEffect will save to Tier 2 (ChatPersistence) // which writes to .keak/history/history.json // ===== Step 10: Tier 1 updates ===== useKeakStore.getState().setGenerating(false, 'complete'); } catch (error) { setIsGenerating(false); useKeakStore.getState().setGenerating(false, 'idle'); throw error; } }; ``` ### State Flow Visualization ``` User clicks "Generate" ↓ Tier 3: setIsGenerating(true), setMessages([...]) ↓ Tier 1: setGenerating(true, 'reasoning') ↓ KeaFab API call (streaming) ↓ Tier 3: setMessages([...result]), setVariants([...result]) ↓ FileStateManager: applyPreview(variant.code) ↓ Tier 3: setPreviewVariantId(result.id) ↓ Auto-save effect (500ms) ↓ Tier 2: ChatPersistence.saveChat(keakId, completeState) ↓ Disk: .keak/history/history.json updated ↓ Tier 1: setGenerating(false, 'complete') ``` **All three tiers** are updated in a coordinated flow. *** ## Hydration Priority **The story**: When loading state for an element, where do we look first? ### Priority Chain ```typescript async function hydrateElementState(keakId: string): Promise { // ===== Priority 1: ChatPersistence (most durable) ===== const persisted = await chatPersistence.loadChat(keakId); if (persisted) { console.log('[Hydration] Loaded from ChatPersistence'); return persisted; } // ===== Priority 2: In-memory conversationHistory (fast) ===== const inMemory = conversationHistory.get(elementId); if (inMemory) { console.log('[Hydration] Loaded from in-memory history'); return reconstructStateFromConversation(inMemory); } // ===== Priority 3: Fresh initialization (new chat) ===== console.log('[Hydration] Fresh chat initialization'); return createFreshChatState(); } ``` ### Why This Order? | Priority | Source | Survives | Speed | Use Case | |----------|--------|----------|-------|----------| | **1** | ChatPersistence | App restart, drawer close, tab switch | ~10-50ms | Restored conversations | | **2** | conversationHistory Map | Only tab switch (not drawer close) | ~1ms | Fast tab switching | | **3** | Fresh state | N/A | Instant | New conversations | **Scenario 1**: User reopens app and selects previous element - Priority 1 succeeds ✅ (ChatPersistence has full history) - Load time: ~30ms - Result: Full conversation restored **Scenario 2**: User closes drawer, reopens, switches tabs rapidly - Priority 2 succeeds ✅ (conversationHistory still in memory) - Load time: ~1ms - Result: Instant tab switch **Scenario 3**: User selects brand new element - Priority 1 fails (no persisted chat) - Priority 2 fails (no in-memory data) - Priority 3 succeeds ✅ (fresh state) - Load time: Instant - Result: Clean slate with assistant greeting *** ## Critical Design Patterns ### Pattern 1: Snapshot Restoration Before Registration **The most critical pattern** in the entire state system. ```typescript // ❌ WRONG ORDER fileStateManager.registerElement(keakId, filePath, fileContent); // If variant is on disk, it gets captured as control! // ✅ CORRECT ORDER const persistedChat = await chatPersistence.loadChat(keakId); if (persistedChat?.elementSnapshot) { fileStateManager.restoreSnapshot(keakId, filePath, persistedChat.elementSnapshot); } fileStateManager.registerElement(keakId, filePath, fileContent); // Snapshot tells FileStateManager which version is on disk ``` **Why this matters**: Without restoration: ``` App restarts with variant on disk registerElement() doesn't know it's a variant Captures to snapshot.originalCode ❌ Real control code lost ``` With restoration: ``` restoreSnapshot() sets activePreview=true registerElement() knows variant is on disk Captures to snapshot.variantCode ✅ Control code preserved in snapshot.originalCode ``` See [Core Concepts: Snapshot System](./core-concepts.md#snapshot-system-preventing-data-corruption). ### Pattern 2: Debounced Persistence **Purpose**: Prevent excessive disk writes. ```typescript useEffect(() => { const timeoutId = setTimeout(() => { chatPersistence.saveChat(keakId, state); }, 500); return () => clearTimeout(timeoutId); }, [/* all state dependencies */]); ``` **Behavior**: ``` User types message ↓ (0ms) State updates (messages array) ↓ (start 500ms timer) User adds more text ↓ (100ms) State updates again ↓ (cancel previous timer, start new 500ms timer) User adds more text ↓ (200ms) State updates again ↓ (cancel previous timer, start new 500ms timer) User stops typing ↓ (500ms elapsed) Save to disk ✅ (only 1 write instead of 3) ``` **Benefit**: 10 rapid changes → 1 disk write (90% reduction). ### Pattern 3: Tab-Switch Detection During Generation **The problem**: User switches tabs while variant is generating. **Scenario**: ``` 1. User selects element A, starts generation 2. User switches to element B (tab switch) 3. Element A's variant completes (in background) 4. Where does the variant go? ``` **Solution**: Capture the keakId and detect tab switches. ```typescript const handleGenerate = async (goal: string) => { // Capture keakId at generation start const capturedKeakId = selectedElement.keakId; // Generate variant (async, might take 10-30 seconds) const result = await generator.generate(goal); // Check if user switched tabs during generation const currentActiveKeakId = useKeakStore.getState().activeKeakId; if (currentActiveKeakId !== capturedKeakId) { console.warn('[Tab Switch] User switched tabs during generation'); // Save result to persistence (even though tab is inactive) await chatPersistence.saveChat(capturedKeakId, { ...currentState, variants: [...variants, result.variant], messages: [...messages, assistantMessage] }); // Switch back to the original tab (show the result) useKeakStore.getState().selectElement(capturedKeakId); return; } // Normal flow - update current tab's state setVariants([...variants, result.variant]); setMessages([...messages, assistantMessage]); }; ``` **Result**: Variant appears in the correct tab, even if user switched away. *** ## State Synchronization ### Tier 1 ↔ Tier 2 Synchronization **Not automatic** - Tier 1 (Zustand) and Tier 2 (ChatPersistence) are independent. **Sync points**: 1. **Element registration**: ```typescript // Zustand stores element metadata useKeakStore.getState().registerElement(data); // ChatPersistence will be updated on next auto-save ``` 2. **Tab management**: ```typescript // Zustand tracks openTabs useKeakStore.getState().selectElement(keakId); // ChatPersistence doesn't care about tabs (stores all chats) ``` 3. **Conversation state**: ```typescript // React state is source of truth during session setMessages([...messages, newMessage]); // Auto-save syncs to ChatPersistence (500ms later) ``` **Why separate?** - Zustand: Fast UI state, app-wide scope - ChatPersistence: Durable storage, project-specific scope - Different responsibilities, different lifecycles ### Tier 2 ↔ FileStateManager Synchronization **Tightly coupled** - ChatPersistence always includes FileStateManager snapshot. ```typescript // Auto-save includes snapshot const snapshot = fileStateManager.getSnapshot(keakId); await chatPersistence.saveChat(keakId, { ...state, elementSnapshot: snapshot ? { controlCode: snapshot.originalCode, variantCode: snapshot.variantCode, activePreview: snapshot.activePreview } : null }); ``` **On load, snapshot is restored**: ```typescript const persistedChat = await chatPersistence.loadChat(keakId); if (persistedChat?.elementSnapshot) { fileStateManager.restoreSnapshot(keakId, filePath, persistedChat.elementSnapshot); } ``` **Result**: FileStateManager state is **always in sync** with persisted conversations. *** ## Multi-Element Conversations ### The Challenge Users often optimize **multiple related elements** (e.g., hero headline + CTA button). **Problem**: Each element has its own keakId, but they're conceptually related. **Solution**: `conversationHistory` Map tracks related conversations. ### Implementation ```typescript // Tier 3 state const [conversationHistory, setConversationHistory] = useState( new Map() ); interface ConversationData { originalSource: string; messages: Message[]; variants: CROVariant[]; } // Update on variant generation const elementId = `${selectedComponent.tagName}-${selectedComponent.textContent}`; conversationHistory.set(elementId, { originalSource: originalSourceCode, messages: [...messages, newMessage], variants: [...variants, newVariant] }); setConversationHistory(new Map(conversationHistory)); ``` **Key**: `elementId` is generated from **tag + text**, so it's stable even if keakId changes. **Use case**: User optimizes hero headline, then hero CTA. Both conversations are tracked in the same Map, enabling context-aware suggestions. *** ## Tab Management ### Open Tabs Array ```typescript // Tier 1 (Zustand) openTabs: string[] // Array of keakIds // Add tab selectElement: (keakId: string) => { set((state) => { const newTabs = state.openTabs.includes(keakId) ? state.openTabs // Already open, don't duplicate : [...state.openTabs, keakId]; // Add to end return { activeKeakId: keakId, openTabs: newTabs }; }); } // Close tab closeTab: (keakId: string) => { set((state) => { const newTabs = state.openTabs.filter(id => id !== keakId); // If closing active tab, activate the last tab const newActiveKeakId = state.activeKeakId === keakId ? newTabs[newTabs.length - 1] || null : state.activeKeakId; return { openTabs: newTabs, activeKeakId: newActiveKeakId }; }); } ``` **Persisted**: Yes (openTabs is in the `partialize` config). **Result**: When you reopen the IDE, all tabs from your last session are restored. ### Tab Display **Component**: `CRODrawer/ChatTabBar.tsx` ```typescript function ChatTabBar({ openChats, activeKeakId, onSelectChat, onCloseChat }) { return (
{openChats.map((chat) => (
onSelectChat(chat.keakId)} > {/* Tab label: text */} <{chat.elementInfo?.tagName || 'element'}>{' '} {chat.elementInfo?.textContent?.substring(0, 20) || chat.keakId.substring(0, 15)} {/* Close button */}
))}
); } ``` **Example labels**: - `

Create stunning web...` - ``) - Invalid JSX: `{value > 0 &&
}` (missing closing tag) - Missing imports: Used component without import #### Solution 2: Manual Fix 1. Click "Hide Preview" (revert to control) 2. Copy variant code from drawer 3. Open in Monaco editor 4. Fix syntax error 5. Save 6. Click "Preview" with edited code #### Solution 3: Regenerate **If unfixable**: - Click "Regenerate" button - Or provide more specific prompt - Or report issue (helps improve AI) *** ### Auto-Repair Failed **Symptoms**: - Auto-repair attempted once with Keafab - Repair failed - **Original code automatically restored** (variant reverted) - Dev server recovers (build error cleared) - Modal: "Build Error Reported" with error details - Auto-report submitted to GitHub automatically **What Changed**: - Old system: 3 attempts with GPT (pattern-based), broken code stayed on disk - New system: 1 attempt with Keafab (full file context), auto-revert on failure - More reliable repair with automatic recovery - Dev server always returns to working state **Auto-Revert Behavior**: - When repair fails, original code is automatically written back to disk - Preview state cleared (variant no longer active) - Dev server HMR reloads with working code - User can regenerate from clean state **Solutions**: #### Solution 1: Report Issue **Click "Report Issue"** button (auto-reports to GitHub): - Creates issue with variant code - Keak team will investigate - AI quality improves for next time #### Solution 2: Regenerate **Start fresh with working code**: - Original code already restored automatically - Click "Regenerate" button in modal - Or edit prompt and try again - Dev server is working (no build error) #### Solution 3: Manual Edit **Expert users** can fix manually: 1. Copy variant code 2. Paste into local editor (VS Code) 3. Fix syntax error 4. Copy fixed code back to IDE 5. Edit variant in drawer 6. Preview again *** ## Experiment Issues ### Experiment Not Syncing to Dashboard **Symptoms**: - Applied variant locally - Created experiment wrapper - But experiment doesn't appear in Dashboard **Solutions**: #### Solution 1: Verify Domain Linked **Setup Panel** should show: ``` Domain Status: ✅ Linked to example.com ``` **If not linked**: - See [Domain Linking Issues](#domain-not-linking) above #### Solution 2: Check Experiment Key **Inspect source file**: ```jsx // Key should be here ... ``` **Dashboard expects** this exact key. If missing: - Experiment wrapper might be malformed - Re-apply variant #### Solution 3: Manual Sync **In Experiments Panel**: - Find experiment (shows as "draft") - Click "Sync to Dashboard" button - Manually configure and launch *** ### Telemetry Not Tracking **Symptoms**: - Variants generated - But no data in Dashboard analytics - Telemetry DB empty **Solutions**: #### Solution 1: Check Network Tab **DevTools → Network**: - Filter by "telemetry" - Look for POST requests to `/telemetry/*` - Should return 200 OK **If 401 Unauthorized**: - Auth token expired - Re-login to IDE **If 500 Server Error**: - Dashboard API issue - Report to Keak support #### Solution 2: Verify Telemetry Service **DevTools console**: ```typescript // Enable telemetry debugging localStorage.setItem('keak-telemetry-debug', 'true'); // Reload IDE ``` **Should see logs**: ``` [Telemetry] generation-start { chatId: '...', userPrompt: '...' } [Telemetry] generation-complete { generationId: '...', ... } ``` **If not logging**: - telemetryService.ts might be disabled - Check service initialization *** ### SDK Import Errors **Symptoms**: - TypeScript errors: "Cannot find module '@keak/sdk'" - Build fails after applying variant - Experiment wrapper code is red in Monaco **Solutions**: #### Solution 1: Install SDK ```bash npm install @keak/sdk # or yarn add @keak/sdk ``` #### Solution 2: Restart TypeScript Server **In VS Code** (if using external editor): - Cmd+Shift+P → "TypeScript: Restart TS Server" **In Keak-Code**: - Close and reopen project #### Solution 3: Check package.json ```json { "dependencies": { "@keak/sdk": "^1.0.0" // Should be present } } ``` **If version mismatch**: ```bash npm install @keak/sdk@latest ``` *** ## Performance Issues ### Slow Element Selection **Symptoms**: - Click element → 5-10 seconds until drawer opens - Terminal shows: "Searching files..." **Cause**: Babel plugin not working (fallback to file search). **Solution**: Fix Babel plugin (see above). *** ### High Memory Usage **Symptoms**: - IDE sluggish - Electron process using >2GB RAM - App freezes occasionally **Solutions**: #### Solution 1: Clear Chat History **History Panel** → Select old chats → "Delete" button. **ChatPersistence** grows over time. Deleting old conversations frees memory. #### Solution 2: Reduce Open Tabs **Close unused chat tabs**: - Click "×" on tab chips - Keeps only active conversations in memory #### Solution 3: Restart IDE **If memory leak suspected**: 1. Save all work 2. Close IDE 3. Reopen IDE 4. Memory resets *** ## Data Loss Prevention ### Chat History Disappeared **Symptoms**: - Had previous conversations - Now history is empty - `.keak/history/history.json` missing or empty **Recovery**: #### Solution 1: Check File Exists ```bash cat .keak/history/history.json ``` **If file missing**: - Might have been deleted - Check if `.keak/` in `.gitignore` (shouldn't be committed) - No automatic backup (local file only) #### Solution 2: Check Git History **If committed before**: ```bash git log -- .keak/history/history.json # Find commit with history git show :.keak/history/history.json > history-recovered.json # Restore from commit ``` #### Solution 3: Prevention **Backup periodically**: ```bash cp .keak/history/history.json .keak/history/history.backup.json ``` **Or commit .keak/** (optional): ```bash # Remove from .gitignore git add .keak/ git commit -m "Backup conversation history" ``` *** ### Variant Code Lost After App Restart **Symptoms**: - Had variant previewed - App crashed or closed - Reopened → variant code is wrong or missing **Diagnosis**: **Check snapshot** in `.keak/history/history.json`: ```json { "elementSnapshot": { "controlCode": "...", // Should have control "variantCode": "...", // Should have variant "activePreview": true // Should indicate which is on disk } } ``` **If elementSnapshot is null**: - Auto-save didn't run before crash - Max 500ms of data loss (debounce window) **Recovery**: - Regenerate variant (use same prompt from messages array) - Or manually edit control code **Prevention**: - Manually save important changes: Cmd+S - Auto-save runs every 500ms, so keep working - Chat history is durable (file-based) *** ## Error Messages Explained ### "Failed to resolve source file" **Meaning**: IDE couldn't find which file defines the element. **Solutions**: - Check if file was moved/renamed - Verify Babel plugin is working - Try selecting element again - Manually select source file (dialog will appear) *** ### "No active preview to hide" **Meaning**: Clicked "Hide Preview" but no variant is currently on disk. **Cause**: State mismatch (activePreview flag is wrong). **Solution**: ```typescript // Reset snapshot window.__KEAK_DEBUG__.fileStateManager.clearElement('element-id'); // Re-select element ``` *** ### "Experiment wrapper not found" **Meaning**: Trying to select winner but `` wrapper is missing in source. **Cause**: - Experiment was manually edited/removed - Code was reformatted and wrapper lost - Wrong file selected **Solution**: - Search for experiment key in codebase: ```bash grep -r "cro-1706123456789" src/ ``` - If found: Use correct file - If not found: Re-apply variant *** ### "KeaFab API timeout" **Meaning**: Variant generation took >60s and was aborted. **Causes**: - Network slow - KeaFab overloaded - Request too complex **Solutions**: - Retry with same prompt - Simplify prompt (more specific, less context) - Try different tier (turbo is faster) - Check network connection *** ## Getting Help ### Before Reporting Issues **Collect these details**: 1. **Error message** (exact text) 2. **Terminal output** (last 50 lines) 3. **Steps to reproduce** 4. **Project framework** (Next.js, Vite, etc.) 5. **Keak-Code version** (Help → About) ### Where to Report - **GitHub Issues**: [keak/keak-code/issues](https://github.com/keak/keak-code/issues) - **Discord**: [keak.com/discord](https://keak.com/discord) - **Email**: support@keak.com ### Debug Information Export **Export debug info** for support: ```typescript // DevTools console const debugInfo = { version: window.__KEAK_VERSION__, project: useProject.getState(), snapshots: window.__KEAK_DEBUG__.getAllSnapshots(), chat: await window.__KEAK_DEBUG__.chatPersistence.loadChat('element-id') }; console.log(JSON.stringify(debugInfo, null, 2)); // Copy and paste into GitHub issue ``` *** ## Related Documentation - **[Setup](./setup)** - Initial configuration - **[Architecture](./architecture)** - System design (helps diagnose issues) - **[Advanced Topics](./advanced)** - Deep dives into edge cases - **[Element Tracking](./element-tracking)** - Selection flow - **[Preview System](./preview-system)** - Swap mechanics - **[State Management](./state-management)** - Persistence and hydration --- /docs/extension-ide/ide/variant-generation # Variant Generation **The Magic**: How a screenshot and a prompt become CRO-optimized code. **Powered by**: [KeaFab API](/docs/ai-infrastructure/keafab/) - Keak's specialized CRO variant generation service. *** ## The User Journey ``` 1. User selects element → CRO drawer opens 2. User sees AI-suggested prompts 3. User enters goal (or selects suggestion) 4. User uploads reference images (optional, max 3) 5. User clicks "Generate" 6. Streaming progress shows (router → optimizer → validator) 7. Variant appears with CRO analysis 8. Auto-preview (with repair if syntax errors) 9. User can: preview, edit, regenerate, or apply ``` **Time**: 10-30 seconds from prompt to variant (depending on complexity). *** ## Step 1: AI-Suggested Prompts ### The Problem Users often don't know **what to ask** for CRO optimization. **Solution**: AI analyzes the element and suggests goal-oriented prompts. ### Implementation **Component**: `CRODrawer/SuggestedPrompts.tsx` **Hook**: `use-prompt-suggestions.ts` ```typescript function usePromptSuggestions({ enabled = true, elementId, // Trigger re-generation on element change selectedComponent, // HTML of selected element sourceCode // Full page JSX source }) { const [suggestions, setSuggestions] = useState([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (!enabled || !selectedComponent) return; const fetchSuggestions = async () => { setIsLoading(true); try { // Call Dashboard proxy → KeaFab suggester const response = await fetch(`${dashboardUrl}/keafab/suggest`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ selected_component: selectedComponent, source_code: sourceCode }) }); const data = await response.json(); if (data.success && data.data?.suggestions) { setSuggestions(data.data.suggestions); } else { // Fallback to default prompts setSuggestions(getDefaultPrompts()); } } catch (error) { console.warn('[Prompt Suggestions] Failed:', error); setSuggestions(getDefaultPrompts()); } finally { setIsLoading(false); } }; fetchSuggestions(); }, [enabled, elementId, selectedComponent, sourceCode]); return { suggestions, isLoading }; } ``` **Dashboard Proxy**: `dashboard-no-code-v3/app/api/keafab/suggest/route.ts` ```typescript export const POST = withAuth(async (user, request: NextRequest) => { const body = await request.json(); const { selected_component, source_code } = body; // Proxy to KeaFab suggester const upstreamResponse = await fetch(`${keafabUrl}/cro/suggest`, { method: 'POST', headers: { 'Authorization': `Bearer ${keafabApiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ selected_component, source_code }) }); const data = await upstreamResponse.json(); return successResponse(data, 200, origin); }); ``` **KeaFab Suggester**: `KeaFab-API/app/api.py` → `/cro/suggest` endpoint ```python @router.post("/suggest", response_model=SuggestResponse) async def suggest_prompts( request: SuggestRequest, _: bool = Depends(verify_api_key), ) -> SuggestResponse: """Generate goal-oriented prompt suggestions for a component.""" suggester = create_suggester(CONFIG.suggester) result, usage = suggester.suggest( selected_component=request.selected_component, source_code=request.source_code, ) return SuggestResponse( suggestions=result.get("suggestions", ["Optimize for maximum conversions"]), usage=usage ) ``` ### Default Prompts If AI suggestions fail or are loading, **fallback to static prompts**: ```typescript function getDefaultPrompts(): string[] { const prompts: string[] = []; // Event-based prompts (if events detected) if (events.length > 0) { const firstEvent = events[0]; const eventMention = `@${firstEvent.label.toLowerCase().replace(/\s+/g, '-')}`; if (firstEvent.type === 'click') { prompts.push(`Improve the ${eventMention} button to increase conversions`); } else if (firstEvent.type === 'link') { prompts.push(`Make the ${eventMention} link more prominent and clickable`); } } // Generic prompts prompts.push('Improve the overall design to boost engagement'); prompts.push('Optimize the copy to be more persuasive'); prompts.push('Make the call-to-action more compelling'); return prompts.slice(0, 4); // Max 4 prompts } ``` **Example AI suggestions** (for a hero button): - "Increase sign-up conversions with clearer value proposition" - "Reduce friction by emphasizing the free trial" - "Make the CTA more action-oriented and urgent" - "Add social proof to build trust" ### UI Display ```typescript
{isLoading && } {!isLoading && suggestions.map((prompt, i) => ( ))}
``` **User experience**: Click a suggestion → auto-fills input → click Generate. *** ## Step 2: Variant Generation ### User Submits Prompt **Action**: User enters goal and clicks "Generate" (or selects a suggestion). **Hook**: `use-cro-actions.ts` → `handleGenerate()` ```typescript const handleGenerate = async ( goal: string, isRetry: boolean = false, uploadedImages: string[] = [] ) => { // 1. Determine generation type const generationType = determineGenerationType(isRetry, variants.length); // → 'INITIAL', 'RETRY', or 'FOLLOWUP' // 2. Mark previous variant if exists if (chatLastVariantId) { if (generationType === 'RETRY') { await telemetry.trackVariantRetry(chatLastVariantId); // Outcome: REPLACED } else if (generationType === 'FOLLOWUP') { await telemetry.trackVariantFollowup(chatLastVariantId); // Outcome: SKIPPED } } // 3. Set generating state setIsGenerating(true); useKeakStore.getState().setGenerating(true, 'reasoning'); // 4. Add user message to chat const newMessages = [...messages, { role: 'user', content: goal }]; setMessages(newMessages); // 5. Upload images to Vercel Blob (if provided) const mediaUrls = await uploadImagesToBlob(uploadedImages); // 6. Track generation start (telemetry) const generationId = await telemetry.trackGenerationStart({ chatId, sessionId, userPrompt: goal, generationType, screenshotUrl, mediaUrls }); try { // 7. Call KeaFab API (streaming) const result = await generator.generate(goal, { selectedComponent, sourceCode, screenshot: screenshotUrl, chatLastVariantId, media: mediaUrls, tier: selectedModel // 'turbo', 'pro', or 'max' }); // 8. Track generation complete await telemetry.trackGenerationComplete({ generationId, variantCode: result.variant.code, analysis: result.analysis, inputTokens: result.usage.input_tokens, outputTokens: result.usage.output_tokens, durationMs: result.durationMs }); // 9. Add assistant message with variant const assistantMessage = { role: 'assistant', content: result.analysis, variant: result.variant }; setMessages([...newMessages, assistantMessage]); setVariants([...variants, result.variant]); // 10. Update conversation history (multi-element tracking) const elementId = `${selectedComponent.tagName}-${selectedComponent.textContent}`; conversationHistory.set(elementId, { originalSource: originalSourceCode, messages: [...newMessages, assistantMessage], variants: [...variants, result.variant] }); // 11. Auto-preview with repair const previewResult = await applier.previewWithRepair(result.variant.code); if (previewResult.success) { setPreviewVariantId(result.variant.id); trackVariantPreview(result.variant.id, result.variant.code); } else { console.error('[Auto-Preview] Failed:', previewResult.error); } // 12. Store variant ID for telemetry linking setChatLastVariantId(result.variantId); // 13. Show native notification new Notification('Variation Ready', { body: `New variant generated for ${selectedComponent.tagName}`, icon: keakLogo }); } catch (error) { console.error('[Generation] Failed:', error); // Show error in UI setMessages([...newMessages, { role: 'assistant', content: `Generation failed: ${error.message}`, error: true }]); } finally { // 14. Clear generating state setIsGenerating(false); useKeakStore.getState().setGenerating(false, 'complete'); } }; ``` ### Generation Types | Type | When | Previous Variant Outcome | Use Case | |------|------|--------------------------|----------| | **INITIAL** | First prompt in conversation | N/A | New element optimization | | **RETRY** | User clicked "Regenerate" button | REPLACED | Not happy with variant, try again | | **FOLLOWUP** | User entered new instructions | SKIPPED | Build on previous variant | **Telemetry tracking** ensures we know which variants were kept vs discarded. *** ## Step 3: KeaFab Integration ### API Call (Streaming) **Hook**: `use-keafab-generator.ts` ```typescript const generate = async (goal: string, options) => { const { selectedComponent, sourceCode, screenshot, chatLastVariantId, media, tier } = options; // Prepare request const request = { full_page: sourceCode, selected_component: selectedComponent.outerHTML, user_prompt: goal || 'Optimize for maximum conversions', // Default if empty screenshot_url: screenshot, media: media || [], tier: tier || 'turbo' }; // Call Dashboard proxy endpoint (streaming) const response = await fetch(`${dashboardUrl}/keafab/generate/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(request) }); // Parse SSE (Server-Sent Events) stream const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let analysis = ''; let variant = null; let usage = {}; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); // Process complete SSE messages for (let i = 0; i < lines.length - 1; i++) { const line = lines[i]; if (!line.startsWith('data: ')) continue; const data = JSON.parse(line.slice(6)); // Remove 'data: ' prefix if (data.type === 'progress') { // Streaming progress update onProgress(data.step, data.message); // → "Classifying component...", "Optimizing hero...", etc. } else if (data.type === 'complete') { // Final result analysis = data.data.analysis; variant = { id: generateVariantId(), code: data.data.variant, analysis: data.data.analysis, changes: data.data.changes, impact: data.data.impact || 'Unknown' }; usage = data.data.usage; } else if (data.type === 'error') { throw new Error(data.message); } } // Keep last incomplete line in buffer buffer = lines[lines.length - 1]; } return { variant, analysis, usage }; }; ``` ### Progress Phases **KeaFab pipeline** (3 phases): | Phase | Agent | Message | Duration | |-------|-------|---------|----------| | **Router** | GPT-4.1-nano | "Classifying component..." | ~1-2s | | **Optimizer** | Gemini-3-flash | "Optimizing hero..." | ~8-20s | | **Validator** | Claude-Haiku-4.5 | "Validating constraints..." | ~2-5s | **UI displays** these in real-time: ```typescript
{generationPhase === 'reasoning' && (
Analyzing element and planning optimizations...
)} {generationPhase === 'applying-edits' && (
Generating optimized variant...
)} {thinkingMessages.map((msg, i) => (
{msg}
))}
``` See [KeaFab Architecture](/docs/ai-infrastructure/keafab/architecture) for pipeline details. *** ## Step 4: Variant Display ### Variant Data Structure ```typescript interface CROVariant { id: string; // Unique variant ID code: string; // JSX/HTML code analysis: string; // CRO diagnosis + strategy changes: string; // Bullet list of changes (COPY/STYLE/STRUCTURE) impact: string; // Expected metric impact generatedAt: number; // Timestamp validationResults?: ValidationResult[]; // Code quality issues } ``` ### Variant Card UI **Component**: `CRODrawer/VariantsList.tsx` → `VariantCard` ```typescript
{/* Preview Indicator */} {isPreview && (
Currently previewing in your app
)} {/* CRO Analysis */}

Analysis

{variant.analysis}
{/* Changes Summary */}

Changes

{variant.changes}
{/* Expected Impact */}
Expected: {variant.impact}
{/* Validation Issues (if any) */} {validationResults.length > 0 && ( Code quality issues detected: {validationResults.map(issue => (
  • {issue.message}
  • ))}
    )} {/* Feedback Buttons */}
    {/* Action Buttons */}
    ``` *** ## Step 5: Auto-Preview with Repair ### Why Auto-Preview? **User expectation**: "I generated a variant, I want to see it immediately." **Challenge**: AI-generated code might have syntax errors. **Solution**: Auto-preview with repair. ### Implementation ```typescript async function previewWithRepair( variantCode: string, maxAttempts: number = 3 ): Promise<{ success: boolean; error?: string }> { let currentCode = variantCode; let attempts = 0; while (attempts < maxAttempts) { // Try to preview const result = await applier.preview(currentCode); if (!result.error) { // Success! console.log(`[Auto-Preview] Success ${attempts > 0 ? `after ${attempts} repair(s)` : ''}`); return { success: true }; } // Preview failed (syntax error, build error) attempts++; console.warn(`[Auto-Repair] Attempt ${attempts}/${maxAttempts}`, result.error); // Call GPT to fix the code const repaired = await gptCodeRepair(currentCode, result.error); if (repaired) { currentCode = repaired; // Update variant in state setVariants(variants.map(v => v.id === variantId ? { ...v, code: repaired } : v )); } else { // Repair failed console.error('[Auto-Repair] GPT repair returned null'); break; } } // All attempts exhausted console.error('[Auto-Repair] Failed after max attempts'); return { success: false, error: `Auto-repair failed after ${attempts} attempt(s)` }; } ``` ### GPT Code Repair ````typescript async function gptCodeRepair( brokenCode: string, error: string ): Promise { const prompt = `You are a code repair assistant. Fix this syntax error in the JSX code: Error: ${error} Code: \`\`\`jsx ${brokenCode} \`\`\` Return ONLY the fixed code, with no explanations or markdown fences.`; try { const response = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [ { role: 'system', content: 'You are a code repair assistant.' }, { role: 'user', content: prompt } ], temperature: 0, // Deterministic repair max_tokens: 2000 }); const repairedCode = response.choices[0].message.content.trim(); // Validate it's actually code (not an explanation) if (repairedCode.includes('```') || repairedCode.includes('Here')) { return null; // GPT gave explanation instead of code } return repairedCode; } catch (error) { console.error('[GPT Repair] API call failed:', error); return null; } } ```` **Success rate**: ~85% of syntax errors fixed automatically. **Fallback**: If repair fails, show BuildErrorReportedModal with manual fix option. *** ## Step 6: User Actions on Variants ### Preview Toggle ```typescript const handlePreview = async (variant: CROVariant) => { const isCurrentlyPreviewing = previewVariantId === variant.id && snapshot?.activePreview; if (isCurrentlyPreviewing) { // Hide preview await applier.clearPreview(); setPreviewVariantId(null); setHasUnsavedChanges(false); } else { // Clear existing preview if different variant if (previewVariantId && previewVariantId !== variant.id) { await applier.clearPreview(); } // Store original if not stored if (!originalSourceCode) { const fileContent = await readFile(sourceFilePath); const boundaries = findElementBoundaries(fileContent, keakId); const elementCode = extractElementCode(fileContent, boundaries); setOriginalSourceCode(elementCode); } // Track preview in telemetry await telemetry.trackVariantPreview(variant.id, codeToPreview); // Preview the variant (or edited version) const codeToPreview = editedVariantCode.get(variant.id) || variant.code; await applier.preview(codeToPreview); setPreviewVariantId(variant.id); setHasUnsavedChanges(true); } }; ``` **Result**: Button toggles between "Preview" and "Hide Preview". ### Apply Variant ```typescript const handleApply = async (variant: CROVariant) => { setApplyingVariantId(variant.id); try { // 1. Apply preview (write variant to disk) const codeToApply = editedVariantCode.get(variant.id) || variant.code; await applier.preview(codeToApply); // 2. Track telemetry await telemetry.trackVariantPreview(variant.id, codeToApply); await telemetry.trackVariantAccept(variant.id); // 3. Track in PostHog posthog.capture('variant_accepted', { variant_id: variant.id, element_type: selectedComponent.tagName, generation_type: generationType }); // 4. Generate experiment key const experimentKey = `cro-${Date.now()}`; // 5. Call parent handler to wrap in await onApplyVariant( originalSourceCode, codeToApply, experimentKey ); // 6. Close drawer onClose(); } catch (error) { console.error('[Apply] Failed:', error); } finally { setApplyingVariantId(null); } }; ``` **Result**: Variant is permanently applied, wrapped in A/B test framework. ### Regenerate ```typescript const handleRegenerate = async () => { // Use the same prompt as before await handleGenerate(userGoal, true); // isRetry=true }; ``` **Result**: Generates new variant with same prompt, marks previous as REPLACED. ### Provide Feedback ```typescript const handleFeedback = async (feedback: 'LIKE' | 'DISLIKE') => { // Update local state setVariantFeedback(prev => ({ ...prev, [variant.id]: feedback })); // Track in telemetry await telemetry.trackVariantFeedback(variant.id, feedback); // Track in PostHog posthog.capture('variant_feedback', { variant_id: variant.id, feedback, element_type: selectedComponent.tagName }); }; ``` **Result**: Thumbs up/down buttons update, feedback tracked for quality metrics. *** ## Miscellaneous Features ### Default Prompt If user clicks "Generate" without entering a goal: ```typescript const userPrompt = goal.trim() || 'Optimize for maximum conversions'; ``` **Result**: Uses generic optimization prompt instead of failing. ### Image Upload Users can upload **up to 3 reference images**: ```typescript const handleImageUpload = async (files: File[]) => { if (files.length > 3) { alert('Maximum 3 images allowed'); return; } // Upload to Vercel Blob const uploadedUrls = await Promise.all( files.map(file => uploadToVercelBlob(file)) ); setUploadedImages(uploadedUrls); }; ``` **Use case**: "Make my CTA button look like this example" (uploads screenshot). **Included in KeaFab request** as `media` array. ### Model Selection Users can choose AI tier: ```typescript ``` **Affects**: - KeaFab model selection (different optimizers for each tier) - Generation speed - Token costs ### Validation Results KeaFab Validator checks 10 constraints. If violations found: ```typescript interface ValidationResult { constraint: string; // e.g., "data-keak-id" passed: boolean; message: string; // Human-readable issue description severity: 'error' | 'warning'; } ``` **Display**: ```typescript {validationResults.filter(r => !r.passed).map(result => ( {result.message} ))} ``` **If errors**: "Apply Variant" button is disabled. **User action**: Edit code manually or regenerate. *** ## Multi-Turn Conversations ### Context Tracking Each generation includes `chatLastVariantId` to enable **context-aware follow-ups**: ```typescript const result = await generator.generate(goal, { selectedComponent, sourceCode, screenshot, chatLastVariantId // ← Enables multi-turn }); ``` **KeaFab uses this** to: - Reference previous variant - Build iteratively on changes - Avoid repeating successful optimizations ### Example Conversation ``` User: Make the CTA more compelling Assistant: [Variant 1] "Get Started Free" (awareness-level optimization) User: Add urgency Assistant: [Variant 2] "Start Your Free Trial Today" (builds on V1, adds urgency) User: Too long, make it shorter Assistant: [Variant 3] "Try Free Now" (builds on V2, shortens) ``` **Without context tracking**: Each prompt would generate from scratch, losing the iterative refinement. *** ## Integration with Other Systems ### Variant Generation → Preview System After generation, variant is **auto-previewed**: ```typescript const result = await generator.generate(goal); // Auto-preview await applier.previewWithRepair(result.variant.code); // → Triggers FileStateManager.applyPreview() ``` See [Preview System](./preview-system) for swap implementation. ### Variant Generation → Telemetry Every step is tracked: ```typescript // Before generation await telemetry.trackGenerationStart(chatId, goal); // After generation await telemetry.trackGenerationComplete(generationId, variant, usage); // On preview await telemetry.trackVariantPreview(variantId, code); // On apply await telemetry.trackVariantAccept(variantId); ``` See [Telemetry](./telemetry) and [Telemetry DB](/docs/ai-infrastructure/database/telemetry/keak-code) for schema. ### Variant Generation → State Management Variants flow through all 3 tiers: ``` Generation completes ↓ Tier 3: setVariants([...variants, newVariant]) ↓ Auto-save effect (500ms) ↓ Tier 2: ChatPersistence.saveChat(keakId, {variants: [...]}) ↓ Disk: .keak/history/history.json updated ``` See [State Management](./state-management) for persistence flow. *** ## Error Handling ### Generation Errors **Errors during generation**: - Network failure (Dashboard unreachable) - KeaFab API error - Timeout (>60s) **Handling**: ```typescript try { const result = await generator.generate(goal); } catch (error) { console.error('[Generation] Failed:', error); // Show error in chat setMessages([...messages, { role: 'assistant', content: `Generation failed: ${error.message}. Please try again.`, error: true }]); // Clear generating state setIsGenerating(false); useKeakStore.getState().setGenerating(false, 'idle'); } ``` **User sees**: Error message in chat, can retry. ### Syntax Errors (Auto-Repair) **Handled by**: `previewWithRepair()` (see above). **Success**: ~85% of errors fixed automatically. **Failure**: Show BuildErrorReportedModal with: - Error details - Auto-issue reporting option - Manual fix guidance ### Build Errors **Dev server compilation errors** after preview: ```typescript // Monitor dev server output (Terminal tab) onDevServerError((error) => { if (error.includes(keakId)) { // Error relates to our variant console.error('[Build Error] Variant caused build failure:', error); // Attempt auto-repair await attemptBuildErrorRepair(error); } }); ``` **Auto-repair** uses GPT with full build error context. **Fallback**: Show error modal, offer to auto-report issue. *** ## Related Documentation - **[KeaFab API](/docs/ai-infrastructure/keafab/)** - AI variant generation pipeline - **[KeaFab Optimizers](/docs/ai-infrastructure/keafab/optimizers)** - Specialized CRO strategies - **[Preview System](./preview-system)** - How auto-preview works - **[State Management](./state-management)** - Variant persistence - **[Telemetry](/docs/ai-infrastructure/database/telemetry/keak-code)** - Event tracking --- /docs/extension/ # Chrome Extension The browser-side studio. Visitors install the extension, log into their workspace, point at any element on any site they own, and the extension generates variants in place, lets them iterate with **Refine / Generate**, then ships the chosen variants as an A/B test against the platform. The actual A/B test, SPRT, billing, and autovariation all run on the **[Platform / Dashboard](/docs/platform/)** — the extension is the *authoring* surface. > **Repo:** [github.com/keak-ai/chrome-extentsion](https://github.com/keak-ai/chrome-extentsion) > · UI lives under `react-panel/` (Vite + React + Zustand). ## User flow Two entry points feed the same test-setup funnel: ![Extension user flow](/diagrams/extension_abstract_user_flow.svg) ### Single-element mode 1. **Click an element** → opens the Element Optimization page. 2. **Optimize** → AI generates variant candidates (full pipeline). When a variant is already shown, the same button becomes **Refine** and runs a single visual-refine pass instead — see [Optimize: Refine vs Generate](/docs/extension/optimize-flow). 3. **Select** the best variant from the History panel — clicking morphs the live DOM in place. See [Variant history & DOM swap](/docs/extension/variant-history). 4. **Edit / re-optimize** as needed; every iteration appends a new variant to History. 5. **Confirm** → adds the active variant to the test configuration. ### Scanner mode 1. **"Get keak suggestions"** → opens the Scanner module. 2. **Set the conversion target** the page should be optimized for. 3. **Launch scan** → the AI analyzes the whole page and proposes a batch of changes. 4. **Filter** the suggestions and select the ones to keep. 5. **Confirm** → adds the selected changes to the test configuration. ### Merge: test setup → launch Both modes converge on the same shared funnel: - **All changes** — review every modification across elements. - **Test setup** — configure name, conversion event, devices, autopilot. - **Launch** — batched `POST /tests/group` against the dashboard. See [Creating a test from selected variants](/docs/extension/creating-tests). ## Architecture at a glance The extension **never calls KeaChrome directly** — every request goes through the dashboard, which owns auth, billing, and persistence and proxies AI work onward to KeaChrome with the backend service key. ``` Page DOM ──► Extension (react-panel) ──► Dashboard /api/v2/page-agent/* (default) │ Dashboard /api/v2/variants/* (legacy in-browser) │ Dashboard /api/tests, /api/tests/group (launch) ▼ │ History panel + local stores ▼ KeaChrome (proxied, +auth +billing) ``` > **Page-agent is the default in practice**, even though the toggle is still labeled > "beta" in the code (`isBetaModeEnabled()`). It removes the in-browser path's hard > constraint that the user must keep the tab open for screenshot capture — the > server drives a Playwright page instead — so it's been the go-to authoring flow > for some time. ## What it owns | Concern | Notes | |---|---| | **Element targeting** | Visual point-and-click → injects a `data-keak-id`, persists a CSS selector for relocation across reloads. | | **Variant authoring** | **Generate** runs the full v2 pipeline; **Refine** runs a single visual-refine pass over the currently-shown variant. See [Optimize: Refine vs Generate](/docs/extension/optimize-flow). | | **In-place preview** | Click any card in the **History** panel to morph the live DOM between Original and any stored variant. See [Variant history & DOM swap](/docs/extension/variant-history). | | **Test creation** | Bundles all selected variants into one batch `POST /tests/group` against the dashboard. See [Creating a test from selected variants](/docs/extension/creating-tests). | | **Local state** | Two parallel stores — Zustand `keak_stored_elements_v1` (metadata + `activeVariantId`) and localStorage `keak_element_states` (raw outerHTML snapshots). | ## Where it sits in the stack Every call lands on the dashboard at `${BACKEND_URL}/...`, which resolves to `https://platform.keak.com/api` in production (or `http://localhost:3000/api` in dev) — see `react-panel/src/lib/constants.ts:42-44`. The dashboard then proxies AI work onward to KeaChrome's `CRO_BACKEND_API_URL` with the service key. | Call | Dashboard endpoint | What the dashboard does | |---|---|---| | **Generate / Refine** (default) | `POST /api/v2/page-agent/generate`, `POST /api/v2/page-agent/refine` | Validates auth + billing, then proxies to KeaChrome's `PageAgentAdapter` — the server drives a Playwright page, so the extension doesn't need to keep the tab open for screenshots. The extension polls the dashboard; the dashboard polls KeaChrome. | | Generate / Refine (legacy in-browser) | `POST /api/v2/variants/generate`, `POST /api/v2/variants/refine` | Older path, used when `isBetaModeEnabled()` is false. Proxied to KeaChrome's `ClientInputAdapter` with uploaded element HTML + a client-captured screenshot — the tab must stay open until the visual-refine screenshot uploads. | | Launch test | `POST /api/tests` (single) or `/api/tests/group` (batch) | Persists `Test` + `TestVariation` rows and serves them to visitors — see [The testing loop](/docs/platform/testing-loop). | > KeaChrome's `/v2/page-agent/*` and `/v2/variants/*` paths exist on the AI engine, > but they're never reachable from a browser — they're behind the dashboard proxy. > See [KeaChrome → Two surfaces, one engine](/docs/keachrome/) for the server-side > distinction. ## Auth & transport - **Auth:** a Supabase JWT obtained at login (`getAuthToken()`), sent as `Authorization: Bearer …`. The dashboard's `withAuth` guard validates every request before it touches KeaChrome or Prisma. - **Transport:** every request is sent through the extension's service worker via `proxiedFetch` so the Supabase JWT travels in the header instead of cookies (the extension's origin can't share cookies with `platform.keak.com`). - **Base URL:** all four constants — `BACKEND_URL`, `URLS.*`, `V2_URLS.*`, `PAGE_AGENT_URLS.*` — derive from `window.keakConfig[environment].backend_url` (`react-panel/src/lib/constants.ts:29-54`). There is no separate KeaChrome URL on the extension side. ## Read next - **[Optimize: Refine vs Generate](/docs/extension/optimize-flow)** — how the action bar picks which pipeline to run from one piece of state. - **[Variant history & DOM swap](/docs/extension/variant-history)** — how clicking a History card morphs the live DOM, and the two-store data model behind it. - **[Creating a test from selected variants](/docs/extension/creating-tests)** — the exact `CreateTestPayload` shape, fan-out across elements + devices, and pre-flight guards. --- /docs/extension/creating-tests # Creating a test from selected variants How the extension turns the user's chosen variants into a `POST /tests` (or `/tests/group`) call against the dashboard backend. ## Entry point and endpoint - API client: `react-panel/src/lib/api.ts:561` — `createTest(payload)` - Endpoint resolution (`api.ts:568-571`): - Single payload → `POST ${BACKEND_URL}/tests` - Array of payloads → `POST ${BACKEND_URL}/tests/group` (batch) - `BACKEND_URL` comes from `URLS.domains` minus the `/domains` suffix (`react-panel/src/lib/constants.ts:67-77`). - Headers: `Content-Type: application/json`, `Authorization: Bearer ` (from `getAuthToken()`). The request goes through `proxiedFetch` → service worker, so cookies aren't required. Call site: `react-panel/src/components/Pages/Common/TestSettingsPage/TestSettingsPage.tsx:1123` ```ts const createdTestsResult = await createTest(payloadsWithChanges); ``` `payloadsWithChanges` is always an array (built by `flatMap`), so this is the **batch endpoint in practice** — even for a single element. See **[Platform → Public API](/docs/platform/api)** for the dashboard's view of this endpoint, and **[The testing loop](/docs/platform/testing-loop)** for what happens to the test after it's created. ## Per-variant payload shape Type definition: `react-panel/src/lib/api.ts:529-559` (`CreateTestPayload`). Runtime construction: `TestSettingsPage.tsx:1038-1059` (`basePayload`). ```jsonc { // ── Identity / routing ──────────────────────────────────────────── "domain_id": 123, // resolved from current domain "name": "My Test - Hero CTA", // testName, suffixed with elementLabel when >1 element, and with device when fanned out "page_url": "https://example.com/path", // element.pageUrl ?? window.location.href "element_query_selector": ".hero .cta", // CSS selector (also sent as a_selector) "type": "CTA", // headline | CTA | copy | text | image | video | html // ── Test config ─────────────────────────────────────────────────── "event_id": 456, // conversion event id chosen on the page "minimum_view_count": 1000, // hard-coded constant "autovariation": false, // autopilot toggle "is_multi_page": false, // "run on all pages" toggle "target_device": null, // null = all devices; otherwise "mobile" | "tablet" | "desktop" // ── The actual variants ─────────────────────────────────────────── // For text-like types (headline/CTA/copy/text/html): // variant_a = original outerHTML, run through stripKeakAttributesFromHtml // variant_b = active variant outerHTML, same stripping // For media types (image/video): // variant_a = element.originalMediaUrl // variant_b = selectedVariant.mediaUrl (falls back to originalMediaUrl) "variant_a": "", "variant_b": "", // Optional inline-style override (only for style-only / combination variants) "variant_a_styles": undefined, "variant_b_styles": "{\"color\":\"#fff\",\"backgroundColor\":\"#000\"}", // ── Selectors & screenshots ─────────────────────────────────────── "a_selector": ".hero .cta", // same as element_query_selector "b_selector": ".hero .cta-variant", // AI-only: selectedVariant.resolvedSelector "screenshot": "https://blob.../page.jpg", // full-page screenshot URL (cached in localStorage[`screenshot-${pathname}`]) "variant_a_screenshot": "https://blob.../original.jpg", "variant_b_screenshot": "https://blob.../variant.jpg", // ── AI provenance ──────────────────────────────────────────────── "b_job_id": "job_abc123", // AI-only: selectedVariant.jobId // ── Free-form context for the dashboard's autoVariationService ──── // Shape varies by element.type — built by the "generationContext" branch // above the payload assembly: "generation_context": { // Text branch (headline/CTA/copy/text): "original_text": "Buy now", "element_type": "CTA", "element_selector": ".hero .cta", "page_url": "https://example.com/path", "screenshot": "https://blob.../page.jpg", "website_description": "…", "max_chars": 24, "max_words": 4, "max_lines": 1, "assessment": { /* cached CRO assessment, if any */ }, // AI variants also carry the pipeline inputs forward: "scope": "section", // selectedVariant.scope (AI only) "custom_prompt": "Make it punchier" // selectedVariant.customPrompt (AI only) } } ``` ### Field-by-field source (text variant, the common case) | Field | Source | |---|---| | `domain_id` | `domain.id` from `useNewDesignFlowStore` | | `name` | `testName` from the form (+ ` - {elementLabel}` when multiple elements, + ` - {Device}` when fanned out) | | `page_url` | `element.pageUrl ?? window.location.href` | | `element_query_selector` | `element.selector` (for `video` elements possibly substituted by the original IMG selector from `generation_context.originalSelector`) | | `type` | `element.type` (`section` is remapped to `html`) | | `event_id` | `event.id` from the selected conversion event | | `variant_a` | Text: `stripKeakAttributesFromHtml(getOriginalHtml(elementId))`, fallback `element.originalText`. Media: `element.originalMediaUrl` | | `variant_b` | Text: `stripKeakAttributesFromHtml(getVariantHtml(elementId, activeVariantId))`, fallback `element.originalText`. Media: `selectedVariant.mediaUrl` | | `variant_b_styles` | `JSON.stringify(selectedVariant.styles)` when present | | `a_selector` | `elementSelector` (same as `element_query_selector`) | | `b_selector` | `selectedVariant.resolvedSelector` — **AI variants only** | | `screenshot` | Page-level screenshot URL, cached at `localStorage["screenshot-{pathname}"]` (regenerated via `takeScreenshot("default", pathname)` if missing) | | `variant_a_screenshot` | `element.originalScreenshotUrl` | | `variant_b_screenshot` | `selectedVariant.screenshotUrl` | | `b_job_id` | `selectedVariant.jobId` — **AI variants only** | | `target_device` | `null` (all devices) when 0/3 selected; `"mobile"`/`"tablet"`/`"desktop"` otherwise | | `generation_context` | Built ad-hoc per element type (see below) | ## Where the "selected variant" comes from The selected variant is whichever variant is currently active for the element — the card highlighted in the History tab. Resolution is identical to the swap logic (see [Variant history & DOM swap](/docs/extension/variant-history)): ```ts // TestSettingsPage.tsx:464-466 const selectedVariant = element.activeVariantId ? element.variants.find((v) => v.id === element.activeVariantId) : undefined; ``` If `activeVariantId` is `null` (Original card active), `variant_b` falls back to the original (text) or `originalMediaUrl` (media), making `variant_a === variant_b`. Those payloads are filtered out before the POST: ```ts // TestSettingsPage.tsx:1090-1100 const payloadsWithChanges = testPayloads.filter( (p) => p.variant_a !== p.variant_b ); if (payloadsWithChanges.length === 0) { toast.error("No changes to test. Please select a variant or create new suggestions."); return; } ``` So **a test is never created for the Original** — the user must have a variant active on at least one element. ## Per-element fan-out and device targeting `storedElements.flatMap(...)` (`TestSettingsPage.tsx:417`) means **one test per element**, but a single element can fan out into multiple payloads: - The user's per-element device selection lives in `useNewDesignFlowStore().ui.deviceSelections[elementId]` (defaults to all three). - `getDeviceTargets(elementId)` (`:409-415`) collapses 0/3 selected → `[null]` (one payload, no device filter) and otherwise returns one entry per chosen device. - Each chosen device produces its own payload (`{ ...basePayload, target_device: device, name: "{base} - {Device}" }`). So 3 elements × 2 devices each = 6 payloads in the batch POST. ## Pre-flight checks before posting Performed in `TestSettingsPage.tsx`, in order, before the `createTest` call: 1. **Empty list guard** (`:1079-1085`) — every element produced 0 payloads (no selector / not in DOM) → toast + abort. 2. **No-change guard** (`:1090-1100`) — every payload has `variant_a === variant_b` → toast + abort. 3. **Active-test overlap** (`:1102-1120`) — refreshes `useActiveTestsStore`, then aborts if any payload's selector is already covered by a running test on the same page. The user is told to use "Reoptimize" instead. Only after all three pass does the POST happen. ## Response handling `createTest` (`api.ts:591-608`) expects `{ data: { tests, domain_id } }` and returns `{ tests, domain_id }`. `tests` is a `Test` (single) or `Test[]` (batch). The call site stores `createdDomainId` for the workspace-switch on the success page. ### Error paths | Status | Handling | |---|---| | `403` / limit error | re-thrown for the limit-modal handler | | `401` | `handle401Error()` triggers the auth-error modal | | Other | `reportError("Failed to create test", { payload }, error)` | ## Pointers — file map | Concern | File | Symbol / line | |---|---|---| | Endpoint + transport | `react-panel/src/lib/api.ts` | `createTest` (`:561`) | | Payload type | `react-panel/src/lib/api.ts` | `CreateTestPayload` (`:529`) | | Payload assembly | `react-panel/src/components/Pages/Common/TestSettingsPage/TestSettingsPage.tsx` | `basePayload` (`:1038`), variant\_a/b resolution (`:431-461`), device fan-out (`:1064-1072`) | | Selected variant lookup | `TestSettingsPage.tsx` | `element.activeVariantId` → `element.variants.find(...)` (`:464-466`) | | HTML snapshots | `react-panel/src/services/elementStateService.ts` | `getOriginalHtml`, `getVariantHtml`, `stripKeakAttributesFromHtml` | | Active variant state | `react-panel/src/store/newDesignFlowStore.ts` | `activeVariantId` on `StoredElement` | | Active-test overlap check | `react-panel/src/services/activeTestsService.ts` | `useActiveTestsStore` | | Backend base URL | `react-panel/src/lib/constants.ts` | `URLS.domains` (`:77`) — endpoint is `${URLS.domains.replace("/domains","")}/tests[/group]` | ## Minimal contract to replicate elsewhere 1. Iterate the page's stored elements; for each, look up `activeVariantId` and resolve the corresponding `StoredVariant`. 2. Build `variant_a` from the **original** snapshot (HTML for text-likes, media URL for image/video) and `variant_b` from the **selected variant's** snapshot (same medium). Strip extension-injected attributes from any HTML you send. 3. Attach selectors (`element_query_selector` / `a_selector`, optional `b_selector` for AI), screenshots (page-level + per-variant), AI provenance (`b_job_id` when applicable), and a free-form `generation_context` carrying the inputs the autopilot needs to keep generating future rounds. 4. Drop payloads where `variant_a === variant_b` (i.e. the user is still on Original) before POSTing. 5. Per-element device targeting fans out into separate payloads with the same content and `target_device` set. 6. POST a single payload to `/tests`, an array to `/tests/group`. Auth via bearer token. --- /docs/extension/optimize-flow # Optimize: Refine vs Generate The **Optimize** button on the floating element toolbar dispatches to one of two pipelines depending on whether the selected element is currently showing **Original** or a **variant**. The decision is made client-side from one piece of state: `StoredElement.activeVariantId`. ## The decision `react-panel/src/components/ElementSelector/ElementActionBar.tsx` ```ts // :217 const isPreviewingVariant = currentStoredElement?.activeVariantId !== null && currentStoredElement?.activeVariantId !== undefined; ``` - `activeVariantId === null` (or no `StoredElement` at all) → **Original is visible** → **Generate** flow → button reads "Optimize". - `activeVariantId` is some variant id → **a variant is visible** → **Refine** flow → button reads "Refine". The same value drives the button copy at `:218`. Clicking the "Original" card in History flips `activeVariantId` back to `null` (see [Variant history & DOM swap](/docs/extension/variant-history)), which flips the label back to "Optimize" on the next render. `currentStoredElement` is the page-scoped `StoredElement` matched to the targeted DOM node by its `data-keak-id` (`:201-206`). Never-optimized elements have no entry → `isPreviewingVariant` is falsy → button is "Optimize". ## The dispatch `ElementActionBar.tsx:495` ```ts const handleOptimize = async () => { if (!targetElement || isOptimizing) return; // …guards for nested/parent collisions… const scope = SCOPE_VALUE[optimizationScope]; navigateTo("elementEdit"); if (isPreviewingVariant) { await startRefinement(targetElement, scope); // ← Refine } else { await startGeneration(targetElement, undefined, scope); // ← Generate } }; ``` Both actions live on the v2 generation store: - `useV2GenerationStore().startGeneration` — `react-panel/src/store/v2GenerationStore.ts:340` - `useV2GenerationStore().startRefinement` — `react-panel/src/store/v2GenerationStore.ts:605` ## Two transports — page-agent (default) vs in-browser variants (legacy) Before either function runs its in-browser pipeline, it short-circuits to the **page-agent transport** when beta mode is on. **Beta mode is the go-to in practice** — the toggle is still called "beta" in the code (`isBetaModeEnabled()`), but it has been the default authoring flow for some time because it removes the in-browser path's constraint that the user must keep the tab open for screenshot capture (the server drives a Playwright page instead). ```ts // v2GenerationStore.ts:341 if (isBetaModeEnabled()) { return runPageAgentGeneration({ element, customPrompt, scope, inputImages, set, get }); } // v2GenerationStore.ts:606 if (isBetaModeEnabled()) { return runPageAgentRefinement({ element, scope, set, get }); } ``` | Transport | When it runs | Dashboard endpoints | Where the page lives | |---|---|---|---| | **Page-agent** (default) | `isBetaModeEnabled()` returns true | `POST /api/v2/page-agent/generate`, `POST /api/v2/page-agent/refine` (`react-panel/src/lib/constants.ts:137-138`) | Server-side — KeaChrome's `PageAgentAdapter` drives Playwright. | | In-browser variants (legacy) | beta mode off | `POST /api/v2/variants/generate`, `POST /api/v2/variants/refine` | Client-side — extension captures the screenshot from the live tab. | Both endpoints sit on the dashboard at `${BACKEND_URL}/...` — `https://platform.keak.com/api` in production (`react-panel/src/lib/constants.ts:42-44`). The dashboard wraps every call with auth + billing and proxies onward to KeaChrome with the backend service key. **The extension never calls KeaChrome directly.** The **client-side decision rule is identical** for both transports — `isPreviewingVariant` selects Refine vs Generate before the transport branch is even reached. The remainder of this page describes the legacy in-browser flow stage by stage; the page-agent transport runs the equivalent stages server-side and returns the same shape (`{ job_id }` → poll → final `variant_html`). ## Generate — full pipeline (legacy in-browser path) Triggered when **Original is visible** (or there's no stored element yet). 1. `initializeElementState(elementId, element)` — snapshot the original outerHTML into `localStorage["keak_element_states"][elementId].original`. 2. Apply shimmer, capture page description (`getCombinedDescription()`). 3. `captureElementScreenshot(element, …)` — fresh screenshot of the **original** element (with zoom-out if larger than viewport). 4. `captureCleanOuterHTML(element)` — pristine original HTML (no Keak markers). 5. `POST V2_URLS.generateVariant` — `${BACKEND_URL}/v2/variants/generate` on the **dashboard** (which proxies onward to KeaChrome with the service key) — with `element_html`, `screenshot_url`, `page_description`, `page_url`, `custom_prompt?`, `input_images?`, `scope?`, fingerprint. → `{ job_id }`. 6. Poll the dashboard for job status → receive draft `variant_html` → `storeVariantHtml` + `switchToVariant` (inject into DOM). 7. Screenshot the **rendered** draft → `POST {BACKEND_URL}/v2/variants/{jobId}/rendered-screenshot` (kicks off the visual refine). 8. Continue polling → QA → store final variant via `storeV2Variant`. Phases progress through `starting → polling → injecting → refining → qa → complete`. This is the **full** pipeline: assessment → generation → visual refine → QA. ## Refine — single visual-refine step (legacy in-browser path) Triggered when **a variant is visible** (`activeVariantId != null`). 1. Look up the active variant's HTML: ```ts const originalHtml = getOriginalHtml(elementId); const currentVariantHtml = getVariantHtml(elementId, activeVariantId); if (!originalHtml || !currentVariantHtml) { toast.error("No active variant to refine."); return; } ``` 2. `set({ phase: "refining", … })` — skips `starting → polling → injecting`. 3. Screenshot the **already-injected variant** as it currently renders on the page. 4. `POST V2_URLS.refineVariant` (`/v2/variants/refine`) with `element_html` (original), `variant_html` (current), `rendered_screenshot_url`, `page_description?`, `scope?`, fingerprint. → `{ job_id }`. 5. Poll → receive refined `variant_html` → store as a **new** variant id (`variant-${Date.now()}`) → `switchToVariant` → re-screenshot → `storeV2Variant`. 6. Phase ends at `complete`. ### Key differences from Generate - **No CRO assessment, no candidate generation** — server runs a single visual-refine pass against the existing variant. - **Different endpoint:** `/v2/variants/refine` vs `/v2/variants/generate` (shared job poll/cancel infrastructure). - **Different payload:** sends both `element_html` and `variant_html`; the rendered screenshot is the **variant**, not the original. - **Produces a new variant** appended to History — does not overwrite the variant being refined. The new id becomes the `activeVariantId`. ## Prompt mode — always Generate The prompt-mode panel (`ElementActionBar.tsx:984-988`) always calls `startGeneration(targetElement, prompt, …)`. Even when a variant is currently visible, supplying a user prompt routes through the full Generate pipeline so the prompt can drive candidate generation. Refine has no `custom_prompt` parameter wired through the action bar. ## Pointers — file map | Concern | File | Symbol / line | |---|---|---| | Variant-vs-original decision | `react-panel/src/components/ElementSelector/ElementActionBar.tsx` | `isPreviewingVariant` (`:217`), `buttonLabel` (`:218`) | | Dispatch | `react-panel/src/components/ElementSelector/ElementActionBar.tsx` | `handleOptimize` (`:495`) | | Generate pipeline | `react-panel/src/store/v2GenerationStore.ts` | `startGeneration` (`:340`) | | Refine pipeline | `react-panel/src/store/v2GenerationStore.ts` | `startRefinement` (`:605`) | | Active-variant state | `react-panel/src/store/newDesignFlowStore.ts` | `setActiveVariantForElement` (`:680`) | | Endpoints | `react-panel/src/lib/constants.ts` | `V2_URLS.generateVariant` (`:120`), `V2_URLS.refineVariant` (`:121`) | | API client | `react-panel/src/services/v2GenerationService.ts` | `apiStartV2Generation` (`:150`), `startV2Refine` (`:218`) | | HTML snapshot reads | `react-panel/src/services/elementStateService.ts` | `getOriginalHtml`, `getVariantHtml` | ## Minimal contract to replicate elsewhere 1. Persist `activeVariantId: string | null` per element (`null` = Original). 2. On the optimize button: - Read `activeVariantId` for the current element. - `null` → call **Generate** (send original HTML + original screenshot; full pipeline). - non-null → call **Refine** (send original HTML + current variant HTML + a screenshot of the variant as currently rendered; single visual-refine pass). 3. Always append the refine result as a **new** variant — never mutate the variant being refined — and set the new id as the active variant. This keeps History honest and lets users compare iterations. 4. Drive the button label off the same `activeVariantId` so the UI matches the action the click will dispatch. --- /docs/extension/variant-history # Variant history & DOM swap How the panel swaps the live DOM between the original element and any of its stored variants when a card is clicked in the **History** list. Each card in History corresponds to one entry under `StoredElement.variants` (the checkmark marks the active one). The "Original" card at the bottom is the special case `activeVariantId === null`. ## Data model — two parallel stores Both are keyed by the element's `data-keak-id`. ### A. Zustand — `keak_stored_elements_v1` (metadata) `react-panel/src/store/newDesignFlowStore.ts` · types in `react-panel/src/store/newDesignFlowTypes.ts` (`StoredElement`, `StoredVariant`). Each `StoredElement` holds: - `elementId` — the `data-keak-id` - `selector` — CSS selector used to relocate the DOM node on reload - `variants: StoredVariant[]` — `{ id, label, source, screenshotUrl, text, mediaUrl, styles, changeSummary, … }` - `activeVariantId: string | null` — `null` means "Original is active" - `originalText`, `originalMediaUrl`, `originalStyles`, `originalScreenshotUrl` ### B. localStorage — `keak_element_states` (raw HTML) `react-panel/src/services/elementStateService.ts` ```jsonc { "": { "original": "", "": "" // … } } ``` This is where the actual HTML snapshots used to morph the DOM live. The Zustand store holds metadata only. ## Where the History list is rendered `react-panel/src/components/Pages/ElementEditPage/ElementEditPage.tsx` - The History section is gated by `expandedSections[elementId].history` and only renders when the element is selected (`isSelected`). - The list is `storedElement.variants` reversed (newest first), filtered by `isReoptimizeDuplicateControlRow`. - Each row renders an `ElementCard` (`react-panel/src/components/Shared/ElementCard.tsx`) with `isActive = historyVariant.id === storedElement.activeVariantId`. - The "Original" card uses `isActive = isOriginActive` (i.e. `activeVariantId == null`). Key locations: | Element | Line | |---|---| | Variant row loop | `ElementEditPage.tsx:1456` | | `handleHistoryClick` | `ElementEditPage.tsx:1586`, wired via `ElementCard onClick` (`:1613`) | | Per-row "apply" icon button (decoration-only cards) | `ElementEditPage.tsx:1479` | | Original card | `ElementEditPage.tsx:1672`, `onClick` (`:1680`) | ## What happens on click — pseudocode Both the variant-row click and the Original-card click follow the same two-step pattern. The only difference is the second arg (`variantId` vs `null`). ```ts function onHistoryCardClick(storedElement, historyVariant /* or null for Original */) { const variantId = historyVariant?.id ?? null; // (1) Update Zustand metadata — drives the "isActive" highlight, survives reload setActiveVariantForElement(storedElement.elementId, variantId); // → newDesignFlowStore.ts:680 // → mutates StoredElement.activeVariantId and persists via saveStoredElementsToStorage() // (2) Mutate the live DOM const domElement = findOrRestoreDomElement(storedElement); // → ElementEditPage.tsx:58 — wraps findStoredDomElement(); toasts on miss if (!domElement) return; // Primary path: replace outerHTML from the localStorage snapshot const applied = applyVariant(domElement, storedElement.elementId, variantId); // → elementStateService.ts:415 // → html = getVariantHtml(keakId, variantId) (or getOriginalHtml if variantId === null) // → if root tagName matches: updateElementFromOuterHTML(element, html) // (textUtils.ts:855 — morphs in place, preserves listeners) // → if tagName differs (e.g. IMG↔VIDEO): parentNode.replaceChild(newEl, element); // copies data-keak-id, width/height // → when reverting (variantId === null): strips data-keak-modified so the page's // MutationObserver stops re-applying the variant // → returns the (possibly new) HTMLElement, or null on failure // Fallback path: no HTML snapshot (older sessions, manual edits) if (!applied) { if (variantId === null /* Original */) { if (storedElement.originalText) updateElementTextPreservingStructure(dom, storedElement.originalText); if (storedElement.originalMediaUrl) updateMediaSource(dom, storedElement.originalMediaUrl, storedElement.originalMediaType ?? "image", true); if (storedElement.originalStyles) applyElementStyles(dom, storedElement.originalStyles); } else /* variant */ { if (storedElement.originalStyles) applyElementStyles(dom, storedElement.originalStyles); // reset baseline first if (historyVariant.text) updateElementTextPreservingStructure(dom, historyVariant.text); else if (storedElement.originalText) updateElementTextPreservingStructure(dom, storedElement.originalText); if (historyVariant.mediaUrl) updateMediaSource(dom, historyVariant.mediaUrl, historyVariant.mediaType ?? "image", true); if (historyVariant.styles) applyElementStyles(dom, historyVariant.styles); } } } ``` A higher-level helper `switchToVariant(keakId, variantId)` (`elementStateService.ts:566`) does "find element by `[data-keak-id]` + `applyVariant`" in one call. The History click does **not** use it because it also needs the metadata fallback path when no HTML snapshot exists. ## Why two steps (metadata + DOM) - **Step 1 (`setActiveVariantForElement`)** is the source of truth for the UI. The active card's checkmark and highlight are driven by `historyVariant.id === storedElement.activeVariantId`, and the value is persisted so reopening the panel restores the same active variant. - **Step 2 (`applyVariant`)** is what the visitor actually sees on the page. Without it the panel's "active" state would drift from the rendered DOM. Skipping step 2 produces the "active state moves but DOM doesn't update" bug that motivated the `findOrRestoreDomElement` wrapper (`ElementEditPage.tsx:50-69`). ## Pointers — file map | Concern | File | Symbol / line | |---|---|---| | History UI + click handlers | `react-panel/src/components/Pages/ElementEditPage/ElementEditPage.tsx` | `handleHistoryClick` (`:1586`), variant apply button (`:1479`), Original card `onClick` (`:1680`) | | Active variant metadata | `react-panel/src/store/newDesignFlowStore.ts` | `setActiveVariantForElement` (`:680`) | | HTML snapshot store | `react-panel/src/services/elementStateService.ts` | `getOriginalHtml` (`:365`), `getVariantHtml` (`:384`), `applyVariant` (`:415`), `switchToVariant` (`:566`) | | In-place outerHTML morph | `react-panel/src/utils/textUtils.ts` | `updateElementFromOuterHTML` (`:855`) | | DOM element relocation | `react-panel/src/components/Pages/ElementEditPage/ElementEditPage.tsx` | `findOrRestoreDomElement` (`:58`) — wraps `findStoredDomElement` from `selectorUtils` | | Card component | `react-panel/src/components/Shared/ElementCard.tsx` | `isActive` prop drives the checkmark | | Types | `react-panel/src/store/newDesignFlowTypes.ts` | `StoredElement`, `StoredVariant` | ## Minimal contract to replicate elsewhere 1. A keyed store of original + variant **HTML snapshots** per element (`{ original, [variantId]: html }`). 2. A keyed store of element **metadata** with an `activeVariantId: string | null` field per element. 3. A stable way to locate the live DOM node (a `data-*` id attribute + CSS selector fallback). 4. On card click: - Set `activeVariantId` in metadata (drives `isActive` UI). - Look up the snapshot HTML (`variantId === null` → original). - Morph the live element's `outerHTML` (preserve the id attribute and, on tag changes, replace the node and reattach the id). 5. (Optional) A piecewise fallback (text / media / styles) for cases where no full HTML snapshot exists. --- /docs/intro # Keak, end to end Keak is an **A/B testing platform with generative AI** — an AI suite that replaces a CRO agency. A customer hands over a page; Keak generates variants, tests them on real traffic, lets a sequential test decide the winner, and then generates the **next** variant from what won — continuously, learning as more tests run. **Business model:** pay-as-you-go impressions — **1000 impressions = $5** ($0.005 each). AI generation (variants, images, video) isn't billed separately. This site is the internal map of how that works. Read this page top-to-bottom for the whole picture, then drop into a system when you need depth. ## The product, briefly Keak started as a Chrome **extension** for text-only variations, then grew to full-HTML redesigns, **media variants** (image→image, image→video), and **PDP templates** (design one product page, launch it across all of them). The platform has since converged on a **hands-off** flow: give a URL, and Keak estimates the conversion goals, recommends them, produces an **audit report** with an interactive split-screen preview, 👍/👎 feedback, and a Slack bot — now also reachable from any LLM through the **Keak MCP**. ## The hands-off loop ```mermaid flowchart LR URL[Customer page URL] --> G[Recommend
    conversion goals] G --> R[CRO report:
    generate variants] R --> T[Launch A/B test] T --> D{SPRT decides} D -->|keep running| T D -->|winner| N[Autovariation:
    next variant] N --> T ``` The differentiator is the tail of that loop: **SPRT** picks winners with statistical rigor and **autovariation** turns each winner into the next challenger — see [The testing loop](/docs/platform/testing-loop). ## The three systems The codebase is a dependency stack. Bottom to top: ```mermaid flowchart TB MCP["Keak MCP — conversational client (any LLM)"] EXT["Chrome Extension — in-page authoring studio"] DASH["Platform / Dashboard — auth · billing · tests · SPRT · autovariation · reports"] KC["KeaChrome API — the AI engine: variant generation"] MCP --> DASH EXT --> DASH DASH -->|proxy, +auth +billing| KC ``` | System | Role | Start here | |---|---|---| | **[KeaChrome API](/docs/keachrome/)** | The AI engine. Generates variants (Meridian / Page-Agent+Prism / CRO-V1), recommends goals, compiles PDP templates. | Overview → API reference | | **[Platform / Dashboard](/docs/platform/)** | Wraps KeaChrome with auth + billing + persistence. Owns tests, the SPRT winner decision, the autovariation loop, and the hands-off audit/report flow. | Overview → The testing loop | | **[Chrome Extension](/docs/extension/)** | The in-page authoring studio. Select an element, generate / refine variants in place, then ship them as an A/B test against the dashboard. | Overview → Optimize flow | | **[Keak MCP](/docs/mcp/)** | A thin conversational client over the dashboard API — discover tests, recommend goals, optimize a page, launch tests, toggle autopilot. | Overview → Tools reference | **Source repos:** [keak-ai/KeaChrome-API](https://github.com/keak-ai/KeaChrome-API) · [keak-ai/dashboard-no-code-v3](https://github.com/keak-ai/dashboard-no-code-v3) · [keak-ai/chrome-extentsion](https://github.com/keak-ai/chrome-extentsion) · [keak-ai/Keak-MCP](https://github.com/keak-ai/Keak-MCP) **[Billing & Ops](/docs/billing/overview)** documents the money side — metered impressions, credits, and the admin runbooks (free-tier gate, POC links, reconciliation, changelog). > The **Legacy** section preserves the older Keak Code IDE, KeaFab, and SDK docs. > Those products still exist but their docs aren't actively maintained — treat them > as historical reference. --- /docs/keachrome/ # KeaChrome API The central AI backend for CRO variant generation. A FastAPI service where **Opus decides the layout and the copy** and **mechanical guardrails enforce what must not break**. Everything upstream — the dashboard, the MCP, the extension — ultimately calls KeaChrome to turn a page (or one element) into a redesigned variant. > **Repo:** [github.com/keak-ai/KeaChrome-API](https://github.com/keak-ai/KeaChrome-API) > · authoritative deep-dives live in its `docs/`, linked throughout. ## Two surfaces, one engine KeaChrome can take its inputs from two places. The difference is sandboxed by an **adapter**, so the same pipeline runs unchanged behind either: | Surface | Adapter | Where the page is | Used by | |---|---|---|---| | **Page-agent path** — `/v2/page-agent/*` | `PageAgentAdapter` | **server-side**, a live Playwright page Keak drives | the default — the dashboard's audits, `/optimize`, and autovariation | | Client path — `/v2/variants/*` | `ClientInputAdapter` | the client's browser; the extension uploads element HTML + a screenshot | the legacy extension flow | > **The page-agent path is the default the platform uses.** Server-side generation > over a live page is what powers hands-off CRO; the client path is the older > extension-supplied surface. ## Three pipelines All active, none archived: | Pipeline | What it does | Where | |---|---|---| | **Meridian** | Opus full-HTML draft + marker-protected sanitize + visual self-refine. | behind both surfaces | | **Prism** | A *funnel* — perceive → strategize → execute → QA, each stage one job. Emits edit ops on a live page. | page-agent path only (live page) | | **CRO (V1)** | DSPy element assessment + text-variant generation. Live traffic — don't modify lightly. | `/cro/*` + foundational endpoints | Meridian and Prism both answer the page-agent path; which one runs is selected by `PAGE_AGENT_PIPELINE` (**default `meridian`**; `prism` opts into the funnel). The page *scanner* (`analyze`/`select`) always runs Prism. The foundational `/extract_context` is **not legacy** — it's the business-context extractor every pipeline consumes. See **[Pipelines](/docs/keachrome/pipelines)** for how each one works step by step. ## The shape of the system Two ideas explain almost everything: **1. A request becomes a job.** The heavy V2 endpoints are asynchronous: they return a `job_id` immediately, a `JobScheduler` runs the work on a background thread under concurrency + memory caps, and the client polls for progress and the result. See [Request lifecycle](/docs/keachrome/request-lifecycle). **2. A pipeline composes stages and drives an adapter.** A pipeline is a *composition* of shared, atomic stages — not a fork. The adapter sandboxes where I/O happens, so the same pipeline body runs against a request payload or a live browser page. ## The six nouns Almost everything in `src/ai/` is one of six things. Learn these and the codebase stops looking like 300 files and becomes a small vocabulary used many times: - **Pipeline** — *the workflow* (`src/ai//pipeline.py`). Active: decides which stages run, in what order, and when to ask the adapter for I/O. Implements the `AIPipeline` protocol (`generate`/`refine`, optionally `analyze`/`select`). Never touches a browser or the job store directly. - **Adapter** — *the I/O surface* (`src/ai/adapters/`). Passive: answers "where do inputs come from, where do renders go, what gets cleaned up." `ClientInputAdapter` vs `PageAgentAdapter` (above). - **Stage** — *one atomic step* (`src/ai/stages/`). A mostly-pure function doing one LLM step: build prompt → `call_llm` → parse → typed result. Pipeline-agnostic; stages never call other stages. - **Orchestrator** — *the glue + lifecycle* (`src/ai/base/orchestrator.py`). `BaseOrchestrator(pipeline, adapter)` validates the pairing (a `requires_live_page` pipeline rejects a non-live adapter), owns the cost ledger, and cleans up in a `finally` that runs on success, failure, and cancellation. - **Prompts** — *the words* (`src/ai/prompts/`, organized `shared/`, `meridian/`, `prism/`, …). Strings and template builders only — no logic, no LLM calls. A stage imports a prompt; it never defines one inline. **Prompts never live in docs.** - **Utils** — *deterministic mechanics* (`src/utils/`, HTML in `src/utils/html/`). No LLM, no state: marker injection, the sanitizer, the facts check, palette, scope, op application. The mechanical guardrails. > **The one-line test for where code belongs:** talks to a model → `stages/` or > `context/`; talks to a browser → `browser/`; holds state/concurrency → `ops/`; > deterministic string/HTML work → `utils/`; the words sent to a model → `prompts/`. The whole thing is a **dependency stack** — `api/` on top, `src/llm/` + `src/utils/` at the base — where nothing imports upward. ## The guardrails Opus is bold; deterministic checks in `src/utils/html/` catch the drift. Prompts tell Opus what's enforced so it can be bold *inside* the guardrails: | Guardrail | Protects | |---|---| | `sanitize.py` | Post-gen cleanup: strip noise, recover sacred blocks, restore markers, or fall back to the original. | | `sacred.py` | Recovers forms, nav, legal anchors, and dynamic-value containers the variant dropped. | | `facts.py` | Catches fabricated stats / invented URLs / fake urgency vs the original's fact manifest. | | `palette.py` | Audits design-token (color/font) drift. | | `scope.py` | Enforces COPY-only / DESIGN-only / FULL edit scope (the decision is made once, in Prism Stage 0). | | `preservation/` | Validates ids/refs (dupes, dangling `for=`, tag regressions) and repairs where safe. | ## Models & transport Models are named `provider/model-name` (Vercel AI Gateway convention) and default to **Opus 4.8** for the heavy stages (the strategist, DESIGN/FULL executor, Meridian draft); cheaper models do the supporting work (Sonnet critics, Flash Lite persona + media description). Every call funnels through `src/llm/call()`, which routes to **Surplus** first (~60–90% cheaper) and falls back to **Vercel** on any error (`LLM_ROUTER=vercel` pins the gateway). ## Going deeper The integration references are mirrored here (API reference, request lifecycle, template representation, configuration). For the design depth that changes often, the repo's [`docs/`](https://github.com/keak-ai/KeaChrome-API/tree/main/docs) stays authoritative: - [`architecture.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/architecture.md) — the layered design, the six nouns, full module map. - [`page-agent-prism-architecture.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/page-agent-prism-architecture.md) — the Prism funnel in depth. - [`preservation-system.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/preservation-system.md) — the preservation contract. - [`job-scheduling-and-caches.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/job-scheduling-and-caches.md) — concurrency caps, queue, RSS governor, caches. `CLAUDE.md` (repo root) is the authoritative design contract. Interactive specs are live at `/docs` (Swagger) and `/redoc` when the service runs. ## Read next - **[Pipelines](/docs/keachrome/pipelines)** — Meridian vs Prism, and the funnel steps. - **[Element prep & screenshots](/docs/keachrome/preparation)** — the shared HTML-markering + screenshot capture + overlay-dismissal pipeline every stage consumes. - **[API reference](/docs/keachrome/api-reference)** — every endpoint + request/response shapes. - **[Generation flows](/docs/keachrome/generation-flows)** — `/optimize`, goal recommendation, PDP templates, media. - **[Template representation](/docs/keachrome/templates)** — how a variant is encoded as a portable diff and morphed onto another page. - **[Request lifecycle](/docs/keachrome/request-lifecycle)** — create → poll → cancel, the poll shape, and the job-artifact black box. - **[Configuration](/docs/keachrome/configuration)** — every env var, grouped by what it controls. --- /docs/keachrome/api-reference # API reference Every served route, grouped by pipeline. Heavy V2 routes are asynchronous — see [Request lifecycle](/docs/keachrome/request-lifecycle) for the create → poll → cancel flow. All routes require the `API_KEY` bearer token (`verify_api_key`). Routers are wired in `main.py`; each group lives under `api/`. > KeaChrome is **internal**: only the dashboard calls it (`CRO_BACKEND_API_URL`), > never the public internet. The dashboard adds per-user auth and billing on top. ## Authentication Every route requires a bearer token equal to the server's `API_KEY` ([Configuration](/docs/keachrome/configuration#authentication--llm-transport)). Missing/invalid → `401`. ```bash curl -X POST https:///v2/page-agent/optimize \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"page_url": "https://example.com/landing"}' # → 202 { "job_id": "0d0ad208-…", "status": "queued" } ``` ## Meridian — `/v2/variants/*`, `/v2/scanner/*` `api/meridian/endpoints.py`. | Method | Route | Notes | |---|---|---| | `POST` | `/v2/variants/generate` | Async: `element_html` + `screenshot_url` + `page_description` → full HTML redesign. | | `GET` | `/v2/variants/{job_id}` | Poll status + per-turn variants. | | `POST` | `/v2/variants/refine` | **Synchronous, stateless.** One refine pass with QA; returns an `approved` flag so the client decides whether to loop. | | `POST` | `/v2/variants/{job_id}/rendered-screenshot` | Job-backed refine — supply a rendered screenshot to continue. | | `POST` | `/v2/variants/{job_id}/visual-qa` | Visual-QA poll. | | `POST` | `/v2/variants/{job_id}/cancel` | Cancel the job. | | `POST` | `/v2/scanner/select` | Cheap model ranks sections worth optimising (`SCANNER_SELECT_MODEL`). | | `POST` | `/v2/scanner/analyze` | Opus produces coordinated per-section directives (`SCANNER_MODEL`). | ## Page Agent — `/v2/page-agent/*` `api/page_agent/endpoints.py`. Server-side optimization over a **live Playwright page**. The pipeline behind `generate`/`refine`/`optimize` is whatever `PAGE_AGENT_PIPELINE` selects (default `meridian`; `prism` runs the funnel). The `analyze`/`select` scanner stages always run Prism. | Method | Route | Notes | |---|---|---| | `POST` | `/v2/page-agent/generate` | Async: `page_url` + `selector` → runs the configured pipeline. | | `POST` | `/v2/page-agent/refine` | Async + cancellable. Re-run framed as "improve this variant" against a rendered screenshot. | | `POST` | `/v2/page-agent/optimize` | Async + cancellable, end-to-end: `extract_context` → `analyze` → `generate × N` under one `job_id`. See [Generation flows](/docs/keachrome/generation-flows#the-optimize-compound). | | `GET` | `/v2/page-agent/optimize/{job_id}` | Poll compound state (page context, persona, strategy, per-section variants). | | `POST` | `/v2/page-agent/extract_context` | Page-level business-context + design-system extraction for the live page. | | `POST` | `/v2/page-agent/inject` | Inject a variant into the live page for screenshot capture. | | `POST` | `/v2/page-agent/verify` | `compare_interactions` — CDP-based listener check. | | `POST` | `/v2/page-agent/select` | Element picking (cheap model, identification only). | | `POST` | `/v2/page-agent/analyze` | Page analysis (Opus page-level Prism scanner). | | `POST` | `/v2/page-agent/recommend_goals` | LLM suggests 3–5 conversion goals from a page scan. | | `POST` | `/v2/page-agent/map_goal_prompt` | Map free-text goal → best-matching `conversion_targets` wire string. | | `GET` | `/v2/page-agent/sessions` | List active sessions. | | `POST` | `/v2/page-agent/sessions/{sid}/heartbeat` | Keep a session alive. | | `DELETE` | `/v2/page-agent/sessions/{sid}` | Tear a session down. | ## Templatify — `/v2/templatify` `api/templatify/`. | Method | Route | Notes | |---|---|---| | `POST` | `/v2/templatify` | **Synchronous, pure-CPU.** `original_html` + `variant_html` → a portable op list the pixel replays on other pages sharing the template. See [PDP templates](/docs/keachrome/generation-flows#pdp-templates-templatify). | ## Jobs — `/v2/jobs` | Method | Route | Notes | |---|---|---| | `POST` | `/v2/jobs/{job_id}/cancel` | Cancel any async job. Cancellation propagates parent → in-flight children. | ## V1 CRO — `/cro/*` `api/cro/`. DSPy element assessment + text-variant generation. **Live traffic — do not modify lightly.** | Method | Route | Notes | |---|---|---| | `POST` | `/cro/extract_context` | Business-context + design-system extraction. | | `POST` | `/cro/assess`, `/cro/assess_batch` | CRO-principle assessment. | | `POST` | `/cro/select` | Element ranking by optimisation potential. | | `POST` | `/cro/generate` | DSPy text-variant generation. | | `POST` | `/cro/scan_and_generate`, `/cro/assess_and_generate` | Combined flows. | ## Foundational — consumed by every pipeline `api/foundational/`. | Method | Route | Notes | |---|---|---| | `POST` | `/extract_context` | Business-context extraction. **Not legacy** — every pipeline consumes its output. | | `POST` | `/select_elements`, `/generate_variant` | Element selection + single-variant generation (kept for back-compat). | | `POST` | `/set_api_key` | Client API-key rotation. | ## Request & response shapes **The async pattern (all heavy V2 routes).** POST returns a job handle immediately; poll the GET route until terminal. The full field-by-field poll record (statuses, `progress[]`, live-preview fields, `versions[]`) is in [Request lifecycle](/docs/keachrome/request-lifecycle#what-a-poll-returns). ```jsonc // POST /v2/page-agent/generate // → 202 { "job_id": "0d0ad208-…", "status": "queued", "session_id": "pending" } // GET /v2/page-agent/{job_id} (poll) { "status": "complete", "result": { "variant_html": "…", "title": "…", "change_summary": "…", "reasoning": {"copy":"…","layout":"…","visual":"…"}, "confidence": "high", "variant_id": "variant-…", "resolved_selector": "#hero", "original_html": "…" }, "latest_variant_html": "…", "rendered_screenshot_url": "https://blob/…", "total_cost_usd": 0.41, "error": null } ``` **The `/optimize` compound** — the whole-page flow the dashboard and MCP run. Request body: | Field | Type | Req | Default | Notes | |---|---|---|---|---| | `page_url` | string | ✅ | — | Server opens this with Playwright. | | `client_session_id` | string | — | auto UUID4 | Reused across calls to share the page-bootstrap cache; echoed back on poll. | | `custom_prompt` | string | — | `null` | **Highest-priority** steering, forwarded to every section generate. | | `conversion_targets` | string | — | `null` | One of three wire forms (below); malformed → `400`. | | `max_sections` | int | — | `5` (clamped `[1,7]`) | Upper bound on sections analyze returns. | | `pipeline` | `prism`|`meridian` | — | `meridian` | Per-section pipeline; **analyze always uses Prism**. | | `scope` | `COPY`|`DESIGN` | — | `null` (both) | Forwarded to each section generate. | | `input_images` | string\[] | — | `null` | ≤3 reference image URLs, forwarded to each section generate as visual context. See [Reference images](#reference-images-input_images). | Its poll (`GET /v2/page-agent/optimize/{job_id}`) returns a compound — `page_context`, `persona`, `page_strategy`, and a `sections[]` array that lights up independently. Full field reference + cost expectations: see [Generation flows](/docs/keachrome/generation-flows#the-optimize-compound). ## Reference images (`input_images`) The generation and refine routes accept an optional `input_images` — a list of **≤3 reference image URLs** that accompany `custom_prompt` and are fed to the generation/refine LLM as visual context (e.g. a design to emulate or a brand reference). Default `null`; more than 3 → `422`. Accepted on: `POST /v2/variants/generate`, `POST /v2/variants/refine`, `POST /v2/variants/{job_id}/rendered-screenshot`, `POST /v2/page-agent/generate`, `POST /v2/page-agent/refine`, and `POST /v2/page-agent/optimize` (forwarded to every per-section generate). The dashboard downsizes oversized images (longest edge > 1568px, or payload > ~1.5MB) and re-hosts the smaller copy before forwarding, so the URLs KeaChrome receives are already within the vision model's budget. ## Interactive specs Always live at **`/docs`** (Swagger) and **`/redoc`** when the service is running. --- /docs/keachrome/configuration # Configuration & environment Every env var KeaChrome reads, grouped by what it controls. Defaults are the literal fallbacks from the `os.getenv(name, default)` calls in code — `—` means no default (unset → `None`). This page mirrors the most operationally relevant slice; the **authoritative, exhaustive list** (with callsites and a re-audit recipe) lives in the repo's [`docs/environment-variables.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/environment-variables.md). > Secrets are never committed. `.env` is gitignored; production secrets live in > Railway. Only placeholders go in any committed example file. ## Authentication & LLM transport | Var | Default | Req | Controls | |---|---|---|---| | `API_KEY` | — | ✅ | Bearer token every `/v2/*` and `/cro/*` request must send (`api/auth.py`). | | `OPENAI_API_KEY` | — | ✅ | Asserted at import (DSPy/litellm fallback). | | `GEMINI_API_KEY` | — | ✅ | Every Gemini-routed call (Flash Lite facts/media/scanner; Pro for V1). | | `LLM_ROUTER` | `surplus` | — | Transport policy: `surplus` (Surplus→Vercel fallback), `vercel` (gateway only — kill-switch), `litellm` (no gateway). | | `SURPLUS_ROUTER_API_KEY` | — | rec | Surplus Intelligence key — the **default** transport (~60–90% cheaper). Present → every `call()` tries Surplus first, falls back to Vercel on error. | | `VERCEL_API_KEY` | — | rec | Vercel AI Gateway key — the **fallback**, and the only transport under `LLM_ROUTER=vercel`. Auto-copied into `ANTHROPIC_API_KEY`. | | `BLOB_READ_WRITE_TOKEN` | `""` | prod | Vercel Blob for run logs. Empty disables uploads (`log_url` → null). | | `SCREENSHOT_BLOB_READ_WRITE_TOKEN` | `""` | prod | Vercel Blob for rendered variant screenshots. | Every V2 call funnels through `src/llm/call()`, which tries the routers `decide_chain()` returns and falls back on any error. Call sites always use the canonical `provider/model-name` id; the Surplus adapter strips the prefix and computes cost from a cached price sheet (~15-min TTL). ## Model routing Defaults skew **Opus 4.8 for the heavy stages**, cheap models for the supporting work. Override any to A/B a model or cut cost. | Var | Default | Stage | |---|---|---| | `MERIDIAN_MODEL` | `anthropic/claude-opus-4.8` | Meridian draft/refine. | | `MERIDIAN_CLEANUP_MODEL` | `google/gemini-3.1-flash-lite` | Meridian cleanup pass. | | `COPY_ONLY_MODEL` | `google/gemini-3.1-flash-lite` | COPY-scope rewrites. | | `SCANNER_MODEL` | `anthropic/claude-opus-4.8` | `/analyze` coordination (Opus). | | `SCANNER_SELECT_MODEL` | `google/gemini-3.1-flash-lite` | `/select` (identification only). | | `PAGE_AGENT_GENERATOR_MODEL` | `anthropic/claude-opus-4.8` | Prism Stage 2 strategist + Stage 3 DESIGN/FULL executor. | | `PAGE_AGENT_COPY_MODEL` | `anthropic/claude-sonnet-4.6` | Prism COPY-scope strategist. | | `PAGE_AGENT_REFINE_MODEL` | `anthropic/claude-haiku-4.5` | Prism refine executor. | | `PAGE_AGENT_QA_REVIEW_MODEL` | `anthropic/claude-sonnet-4.6` | Prism Stage 4 unified review (breakage repair + leak cleanup). | | `MEDIA_DESCRIBE_MODEL` | `MERIDIAN_CLEANUP_MODEL` | Vision model that describes images/canvas. | | `LLM_MODEL_OVERRIDE` | — | **Pins every call to one model.** Set to a Flash Lite id for cost-controlled wiring tests (quality drops markedly). | `PAGE_AGENT_PIPELINE` (default `meridian`; `prism` opts into the funnel) selects which pipeline answers the page-agent path — see [Pipelines](/docs/keachrome/pipelines). ## Scheduler & memory (JobScheduler + RSSGovernor) Single source of truth for async-job capacity. See [Request lifecycle](/docs/keachrome/request-lifecycle#scheduling-gates). | Var | Default | Prod | Controls | |---|---|---|---| | `MAX_CONCURRENT_JOBS` | `15` | `40` | Total in-flight async jobs. | | `MAX_CONCURRENT_PER_SESSION` | `5` | `5` | Per-`client_session_id` running cap (others overtake). | | `MAX_QUEUED_JOBS` | `100` | `200` | Past this, submit → `503 queue_full + Retry-After`. | | `RSS_SOFT_PCT` | `70` | `75` | Above this % of `MEMORY_LIMIT_BYTES`, promotions held. | | `RSS_HARD_PCT` | `85` | `88` | Above this %, submit → `503 memory_pressure`. | | `MEMORY_LIMIT_BYTES` | host RAM | container limit | RSS denominator. **Set to the container limit on Railway/Docker** (e.g. `34359738368` = 32 GB) so gates don't compute against the host's view. | ## Browser pool & sessions One Chromium process; per-job `BrowserContext` from the pool. Bounded independently from scheduler concurrency. | Var | Default | Prod | Controls | |---|---|---|---| | `MAX_BROWSER_CONTEXTS` | `25` | `80` | Total in-flight contexts (~40–100 MB each). | | `MAX_BROWSER_PAGES` | `6` | `20` | Simultaneous held pages — effectively caps concurrent renders. | | `MAX_BROWSER_SESSIONS` | `5` | `50` | Per-`client_session_id` identity cap (eviction past this). | | `SESSION_TTL_MINUTES` | `15` | `10` | Idle session expiry. | | `ORPHAN_TTL_SECONDS` | `1800` | `900` | Orphaned `sessions/` folders swept after this. | ## Preprocess caches Four in-memory TTL+LRU caches in front of Prism Stage 0/0a/1. Keyed by `(session, page_url)` / `(page_url, selector)` / persona / critique — deliberately **not** by page HTML (dynamic pages broke reuse). | Var | Default | Prod | Controls | |---|---|---|---| | `CACHE_TTL_SECONDS` | `600` | `900` | Per-entry TTL across all four caches. | | `CACHE_MAX_ENTRIES` | `200` | `800` | LRU cap per cache. | ## Cross-service: error hub KeaChrome POSTs its own unhandled failures to the dashboard error hub (`{DASHBOARD_URL}/api/errors`). **Dormant by default** — with no secret it's a silent no-op, which keeps tests network-free. | Var | Default | Controls | |---|---|---| | `INTERNAL_API_SECRET` | — | `x-api-secret` header. Must match the dashboard's exactly. Unset/blank → reporter disabled. | | `DASHBOARD_URL` | `http://localhost:3000` | Dashboard origin, e.g. `https://platform.keak.com`. **Not** `API_BASE_URL` (that's KeaChrome's own public URL). | ## Dev / debug | Var | Default | Controls | |---|---|---| | `DEV_MODE` | `0` | `1` dumps per-job artefacts under `tests/_artifacts/` and keeps session folders; disables Vercel log upload (logs → console). | | `API_BASE_URL` | `http://localhost:8000` | Base URL the API uses to synthesize screenshot URLs for itself. | | `EXTRACT_CONTEXT_SETTLE_MS` | `1500` | Max extra wait before the extract\_context screenshot while CSS animations settle. `0` disables. | V1/DSPy model vars (`GENERATION_MODEL`, `CONTEXT_MODEL`, `ASSESSMENT_MODEL`, `FALLBACK_MODEL`, …) and soak-test overrides exist for the legacy `/cro/*` path and are listed in full in the repo doc. A handful of older vars (`PAGE_AGENT_MODEL`, `PAGE_AGENT_MODE`, `DESIGN_ONLY_MODEL`, `PAGE_AGENT_FIXER_MODEL`) are **removed** — drop them if you find them in old deployment configs. --- /docs/keachrome/generation-flows # Generation flows The handful of higher-order flows worth understanding beyond the single-element `generate`. For the raw endpoint list see the [API reference](/docs/keachrome/api-reference). ## The /optimize compound `POST /v2/page-agent/optimize` is the **hands-off, whole-page** flow — the one the dashboard's audit/CRO report and the MCP's `create_cro_report` run under the hood. It is a parent job that wires three stages under one `job_id`: ``` extract_context → analyze → generate × N (one per selected section) (page bootstrap) (page strategy + (parallel, each inherits the page per-section strategy + its section directive) directives) ``` The poll returns a `compound` object: `page_context`, `persona`, `page_strategy`, a `sections[]` array (each with its own `selector`, `directive`, `status`, `job_id`, and mirrored `result`), and `child_job_ids[]`. It **completes when ≥1 section produces a variant**; it fails only if `extract_context`/`analyze` fail or every section fails. Cancellation **fans out** parent → all in-flight children. Handler: `api/page_agent/endpoints.py` (`@router.post("/optimize")`). Client contract detail lives in the repo's [`docs/page-agent-optimize-client.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/page-agent-optimize-client.md). ## Goal recommendation Two LLM endpoints turn a page into conversion goals (`src/ai/goal_recommender/`): - `POST /v2/page-agent/recommend_goals` → `recommend.py::recommend_goals()` — scans the page, suggests **3–5 goals** (each a title, description, kind, and a mapped wire string). - `POST /v2/page-agent/map_goal_prompt` → `map_prompt.py::map_goal_prompt()` — maps a free-text user goal to the single best-matching wire string from the scanned pool. ### `conversion_targets` wire format `analyze`/`optimize` accept a `conversion_targets` value — a single **string** carrying **one** of three forms (anything else → `HTTP 400`; parser: `src/ai/scanner/conversion_targets.py`): ```jsonc // 1. Shopify purchase — the literal "conversion_targets": "shopify_purchase" // 2. Page view — " ", match_type ∈ {exact_match, starts_with, dynamic} "conversion_targets": "starts_with https://example.com/checkout" "conversion_targets": "dynamic https://example.com/product/:id" // :id matches /product/123, /abc… // 3. CTA list — JSON array of anchors, each with at least a selector "conversion_targets": "[{\"selector\":\"#take-quiz\",\"content\":\"Take the Quiz\",\"type\":\"button\"}]" ``` CTA anchors are *sacred*: analyze walks up to the anchor's natural parent section, picks that parent, and the generator preserves the anchor element verbatim while optimizing around it. ## PDP templates (templatify) `POST /v2/templatify` (synchronous, pure-CPU) compiles an `(original_html, variant_html)` pair into a **portable op list** — atomic structural/style changes keyed by CSS selector that resolve on any page sharing the template. This is how a variant designed on one product detail page (PDP) launches across **all** PDPs. - `preserve_content=true` (default) → **cross-product template**: two passes (`_compile_raw` → `filter_template_ops`) strip product copy, so Product B inherits A's *design* while keeping B's own title/price/reviews. - `preserve_content=false` → every op including content, for globally-identical elements (nav/footer/banner). The op vocabulary is **design-only** — `add_class`, `set_styles`, `set_attrs`, `insert_*`, `remove`, `prepend_style_block` — with **no content ops** (`replace_text` etc. are deliberately excluded). A compact `morph` format (~58% smaller) is also available. → The full encoding (both formats, every field, the selector grammars) and **how a variant is morphed onto another page** (resolver + apply-phase pseudocode) are in [Template representation](/docs/keachrome/templates). ## Media KeaChrome **describes** media, it doesn't generate it. `src/ai/context/media.py` runs a vision call (Gemini Flash Lite by default) to extract per-image descriptions (`[src-N]`, `[svg-N]`, `[bg-N]` markers) and injects them as HTML comments so downstream stages can reason about the imagery without seeing raw URLs or SVG data. Prism Stage 0 always does this; Meridian does it on the visual-refine path. Marker types, the three-step process, the injected comment shape, and the env vars (`MEDIA_DESCRIBE_MODEL`, `MEDIA_DESCRIBE_TIMEOUT`, …) are documented in **[Element prep & screenshots → Media descriptions](/docs/keachrome/preparation#media-descriptions--srcaicontextmediapy)**. The actual **image→image and image→video variant generation** is a dashboard capability, not a KeaChrome one — see [the testing loop](/docs/platform/testing-loop#media-variants). Callers can also **steer** a generation with up to three **reference images** via `input_images` on the generate/refine/optimize routes — image URLs fed to the LLM as visual context (a design to emulate, a brand reference). See [Reference images](/docs/keachrome/api-reference#reference-images-input_images). --- /docs/keachrome/pipelines # Pipelines A pipeline is *the workflow* — it composes shared [stages](/docs/keachrome/#the-six-nouns) into a sequence and drives an [adapter](/docs/keachrome/#two-surfaces-one-engine) for I/O. Two pipelines serve the modern page-agent path; a third (CRO V1) still serves `/cro/*`. Both V2 pipelines implement the same `AIPipeline` protocol and run behind the same `BaseOrchestrator` — **they differ in shape, not in plumbing.** | | **Meridian** | **Prism** | |---|---|---| | Idea | Opus owns the layout — one draft, then visual self-refine | A funnel: each stage has one job | | Sync / async | Sync (dispatched to a thread) | Async (native) | | Live page? | Optional (either adapter) | **Required** (`requires_live_page=True`) | | Output | Full HTML per round | **Edit ops** (JSON) applied to the live DOM | | Conversation | Yes — history across refine rounds | No — one strategy → one execute | | Vision calls | 0–1 (optional visual refine) | 2 parallel critics (Sonnet, vision-only) | **Which one answers `/v2/page-agent/*`?** Selected by `PAGE_AGENT_PIPELINE` — **default `meridian`**, set `prism` to run the funnel. The page *scanner* (`analyze`/`select`) always runs Prism. *** ## Meridian Opus owns the layout: the original HTML is reference material, not a template. The flow is short and conversational, so refine rounds build on history: ``` preprocess → meridian_draft → (render → visual_refine) × N → sanitize / QA ``` - **preprocess** — mechanical element prep + marker protection (`HTMLProcessor`). - **meridian\_draft** — one Opus call returns a full-HTML redesign. - **visual\_refine** (0–N) — render the draft, screenshot it, and let Opus correct what it sees; client-driven, so the caller decides whether to loop. - **sanitize / QA** — the [guardrails](/docs/keachrome/#the-guardrails) run: restore markers, recover sacred blocks, check facts, fall back to the original if unrecoverable. Meridian's hard-won rules live in the repo's [`CLAUDE.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/CLAUDE.md). *** ## Prism — the funnel Prism's premise: one Opus call shouldn't *perceive, strategize, and execute* at once with the whole HTML in the prompt — that yields timid, additive variants and dropped scripts. So it splits the work into stages, and **the HTML appears in only two of them** (Preprocess and Execute). The strategist thinks in CRO primitives; the executor translates a spec and has no CRO opinion. ```mermaid flowchart LR P0["Stage 0
    Preprocess
    (shared context)"] P0a["Stage 0a
    Persona"] P1["Stage 1
    Perceive
    (2 critics)"] P2["Stage 2
    Strategize
    (developer spec)"] P3["Stage 3
    Execute
    (JSON ops)"] P4["Stage 4
    Apply + QA
    (is it broken?)"] P0 --> P0a --> P1 --> P2 --> P3 --> P4 ``` ### Stage 0 — Preprocess · the shared context Deterministic, **no LLM**. Builds the bundle every later stage reads (`PrismContext`): the marker-protected `clean_html` (scripts/JSON/runtime-hooks/media swapped for markers so the model can't corrupt them), `page.md` + `design.md`, the **facts manifest**, the **palette**, the **preservation contract** (incl. live CDP listener evidence), per-image **media descriptions** (Flash Lite), and stable `keak-id` edit targets. Crucially, **scope (COPY / DESIGN / FULL) is decided here** and gates every downstream stage. > The HTML markering, media-description vision call, element-visibility chain, and > screenshot capture all live in the shared preparation pipeline — > see **[Element prep & screenshots](/docs/keachrome/preparation)** for the marker > vocabulary, capture contract, and overlay-dismissal layers. ### Stage 0a — Persona A single Flash Lite call derives a page-level visitor persona (who they are, what they want, what makes them bounce), persisted per `(session, page_url)` and reused across `/analyze`, `/generate`, `/refine`, `/select`. ### Stage 1 — Perceive · two parallel critics Two Sonnet **vision-only** calls (`asyncio.gather`) — **no HTML in either prompt**, just screenshots + persona: - **Visitor critic** — reads the section as the persona would: first impression, scan path, what confused them, would they act. - **CRO critic** — audits against the CRO handbook: strengths, frictions, what's missing for the section's role. They **enrich, not gate** — they produce observations (empty arrays allowed), never ops, selectors, or colors. ### Stage 2 — Strategize The expensive Opus 4.8 step. It reads both critiques + context + screenshot (but **not** the full HTML) and emits a precise **developer spec** — objectives plus per-axis instructions (copy / hierarchy / layout / visual / proof / media) exact enough that a non-CRO developer could implement them without questions. A `custom_prompt` is the highest-priority input. Spec sections are gated by scope; one re-prompt on drift, then flag and proceed (never loop forever). ### Stage 3 — Execute · JSON ops A bounded coder agent (model by scope: COPY → Sonnet, DESIGN/FULL → Opus 4.8) translates the spec literally — it has no CRO opinion. It emits a **13-op JSON array** (`apply_ops`: `add_class`, `set_styles`, `set_attrs`, `insert_*`, `remove`, …) or, for FULL scope, a full-HTML replacement. It has MCP-style tools (`apply_ops`, `read_element`, `capture_screenshot`, `inject_variant`, …) and gets the screenshots only to locate "where" references — not to evaluate. This is the same op vocabulary [templatify](/docs/keachrome/generation-flows#pdp-templates-templatify) compiles for PDP replay. ### Stage 4 — Apply + QA + Render Deterministic + Playwright. QA asks exactly one question — **"is it broken?"** — never "is it good" (that's how QA loops became infinite): 1. `apply_edit_ops` → candidate HTML (or accept the full-HTML replacement) 2. `qa_mechanical` — facts check, preservation contract, fabricated-URL strip, sacred recovery, scope-text-diff; auto-repair where contracts allow 3. restore all markers (scripts, JSON, media URLs) 4. render — Playwright reload + inject + viewport-walk screenshot 5. `qa_soft` — **one** Sonnet vision call flagging only breakage (clipped/overlapping text, missing media, accessibility regressions) 6. **one** auto-repair round if broken; otherwise return `accepted_with_issues` and let the client decide whether to `/refine` `/refine` runs the same funnel end-to-end, except the input HTML is the prior variant, the critics see the *rendered* variant, and `custom_prompt` is the dominant strategic input. *** ## CRO V1 The original DSPy pipeline behind `/cro/*` — element assessment + text-variant generation. Still serves live traffic; new work goes to Meridian/Prism. See the [API reference](/docs/keachrome/api-reference). --- /docs/keachrome/preparation # Element preparation & screenshot pipeline Every variant generation reads two inputs from the live page: **a screenshot of the element in its real viewport** and **the element's HTML, marker-protected and annotated with media descriptions**. Both come from one shared preparation pipeline used by every stage that needs them — Meridian's `element_prep`, Prism's Stage 0, and the post-injection `render` for visual refine. > **Authoritative reference (drift):** the repo's > [`docs/screenshot-pipeline.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/screenshot-pipeline.md). > This page mirrors the integration-relevant contracts; the source has additional > debugging detail (failed approaches, specific bug references, exact code line > numbers). ## The two call-sites, one capture function | Stage | File | When it runs | |---|---|---| | `element_prep.prepare` | `src/ai/preprocess/element_prep.py` | Once per element, before generation — captures the **original**. | | `render.render` | `src/browser/render.py` | After variant injection (pre-QA and again post-QA) — captures the **rendered variant**. | Both delegate to `capture_element_views` (`src/browser/viewport_walk.py:120`) — the single source of truth for element screenshot capture. ## The capture contract — one screenshot, one viewport ``` The element's top edge is scrolled to the viewport top, then the full natural viewport is captured. No zoom-fit. No growing the viewport. ``` A variant must be evaluated at the **same viewport as the original** — different crops scramble the QA's breakage signals. Tall elements clip below the fold; that is the fold a real visitor sees. | Constant | Value | |---|---| | `SCREENSHOT_WIDTH` | 640 px | | `SCREENSHOT_HEIGHT_CAP` | 1200 px | | Resampling | LANCZOS | | Returned zoom (`first_view_zoom_scale`) | always `1.0` | The raw Playwright screenshot is at the browser viewport resolution; `_downsize_screenshot` resizes in place on disk, preserves aspect ratio, and caps height so an unusually tall viewport can't balloon the LLM payload. Element measurement uses `offsetHeight / offsetWidth / offsetTop / offsetLeft` (not `getBoundingClientRect`) — children extending past the parent box can cause the two to diverge. A zero-dimension element returns `[]` (signals "skip — likely `display:none`"), and downstream pipelines treat that as the element being unreachable. ## Capture sequence Before the actual shutter click, four guards run to keep the frame honest: ``` 0. Dismiss overlays → _dismiss_overlays_for_capture 0a. Wait for animations to settle → wait_for_visual_settle (default 1500 ms budget) 0b. Force lazy content visible → _force_content_visible 1. Measure element → _measure (offsetTop/Height/Width) ↳ Zero-layout guard → return [] (display:none / unresolved) 2. Scroll element top to y=0 → window.scrollTo(0, offsetTop) 3. Capture full viewport → page.screenshot(clip={x:0, y:0, vp_width, vp_height}) 4. Downsize to thumbnail → 640-wide, ≤1200 tall ``` ### Visual settle — why `getAnimations()` polling Counters that rise 0→N, fade-ins, and scroll-triggered tweens produce mid-animation frames that downstream extraction records as ground truth (a wrong number as a fact value, partially-rendered copy, etc.). The settle algorithm: 1. Initial wait `min(500, max_wait_ms)` ms — counter libraries fire via `IntersectionObserver` ~100–300 ms after first paint. 2. Poll every 250 ms: `getAnimations().filter(playState='running').length`. 3. Require **two consecutive quiet polls** before declaring settled — one isn't enough because counter libs briefly pause between value bumps. 4. Return `True` on settled, `False` on timeout. Never raises. `EXTRACT_CONTEXT_SETTLE_MS` (default `1500`, `0` disables) controls the page-level settle budget for `extract_context`'s page screenshot; `capture_element_views` takes the same budget as a per-call argument. ### `content-visibility: auto` fix Modern Shopify/Hydrogen themes use `content-visibility: auto` on accordion sections. Until that region scrolls near the viewport, the browser skips layout and reports a `contain-intrinsic-size` placeholder — measurement under-reports the true height. `_force_content_visible` scrolls the element's **bottom** into view first (pulls all `cv: auto` children through the viewport, forcing layout), then scrolls back to the top so the actual capture starts at the element's start. No DOM mutation — one `scrollIntoView` each direction plus short waits. ## Overlay dismissal — four layers `dismiss_overlays` (`src/browser/visibility.py:495`) runs page-level dismissal in priority order; each layer can be a no-op if the prior one already cleared the page. **Cached per page** by `id(page)` — repeat calls on the same page are free. | Layer | Approach | Notes | |---|---|---| | **0. AutoConsent** | DuckDuckGo's library, ~500 per-CMP rules vendored under `vendor/autoconsent/` | Driven from `autoconsent_runner.py`; config: `autoAction: "optOut"`, `enableCosmeticRules: false`. Timeout `autoconsent_timeout_s = 8 s`. | | **1. Vendor fast-path** | Hardcoded selectors for major CMPs (Cookiebot, OneTrust, CookieFirst, TrustArc, Osano, Iubenda, Quantcast, Civic UK, HubSpot, Drupal EU, Complianz, Termly, Klaro, CookieYes, Moove, Cookie Notice for WP, Insites) | Cheap safety net if AutoConsent times out before clicking. Each selector guarded by `el.offsetParent !== null`. | | **2. Structural detection** | `elementFromPoint` at three viewport points (`y=5`, `y=vh/2`, `y=vh-5`) — walks up to outermost positioned ancestor, dismisses if ≥5% viewport coverage | Three sub-passes: (A) corner-X button (small + upper-right + close semantics), (B) universal token match (Accept preferred over Decline so the banner doesn't reappear on next load). Edge samples are required — slim cookie strips at the bottom of a 720px viewport sit below `vh*0.9`. | | **3. Late-popup poll** | Marketing popups fire on `setTimeout 5–10 s` after page-load — poll `_HAS_OVERLAY_JS` every ~500 ms, re-fire layers 1+2 on detection | Deadline resets **only if the overlay actually leaves** (not on any click) — otherwise a site whose handler removes content but leaves the backdrop would spin forever. Bails after 2 ineffective consecutive attempts. | No DOM removal — `backdrop-filter: blur` and stacking context stay honored. Escape dispatch (a former pass C) was removed: some sites' keydown handlers strip modal-content but leave the backdrop applying blur to the page permanently — a stuck blurry page is strictly worse than a visible popup. ## Element-level visibility `ensure_element_visibility` (`src/browser/visibility.py:52`) runs before *both* the original and the variant screenshots — popups occasionally reappear after a DOM morph. Three layers: 1. **Mechanical** — scroll into view → cookie-banner fast-path → if `covered_by_overlay` find close button inside the blocking root → if `scrolled_out_of_container` `scrollIntoView({block:'center'})`. 2. **Flash Lite vision** — if mechanical fails, cheap viewport screenshot → Gemini Flash Lite: *"is `` visible, and if not, what action would reveal it?"* Returns `{visible, action: {type, target}}`. Up to 2 attempts. 3. **Give up** — return `{visible: False, reason, actions_taken: [...]}`. The caller proceeds with whatever screenshot the engine can produce; downstream stages log the obscured state. Never raises. ### Mobile-width fallback When the visibility probe reports `display_none` or `zero_dimensions` at desktop width (1280 px), `element_prep.prepare` retries at **390 px mobile width**: ``` 1. set_viewport_size({width: 390, height: orig_height}) 2. wait 400 ms + re-run ensure_element_visibility 3. visible → capture at 390, record capture_width=390 4. still hidden → restore desktop viewport, abort 5. after capture → always restore viewport ``` `capture_width` is saved on `ElementPrep` so Stage 4 renders the variant at the **same** viewport — otherwise original and variant screenshots wouldn't be comparable. ## Screenshot caching Keyed on `(selector, page_url)`, stored under `workspace/{element_id}/screenshots/`. A cache hit skips `capture_element_views` entirely and rehydrates `views` from the stored PNG. See `_load_screenshot_cache` / `_write_screenshot_cache` in `element_prep.py`. ## HTML preparation — `HTMLProcessor` markers `HTMLProcessor` (`src/utils/html/processor.py`) is applied to the raw `outerHTML` **before** it goes to any LLM. It reduces token count, prevents the model from corrupting URLs or runtime attributes, and lets the model focus on design/copy instead of attribute juggling. `restore(variant_html)` swaps markers back after generation. ### Block-element shells Entire elements replaced with dimension-aware empty shells: | Tag | Shell | Why | |---|---|---| | `` | `` | Path data is meaningless to the LLM; can be 87%+ of HTML (e.g. Stripe hero). | | `` | `` | Empty without JS; dynamic content that breaks layouts. | | `` | `Inline image (WxH) — filename: product-hero.jpg` | | `[bg-N]` | `data-bg-src="[bg-N]"` | `CSS background image on container — filename: banner.jpg` | URL filename hints come from the `attr_store` (the original URL before markering, trimmed to 60 chars with the UUID prefix stripped). `WxH` comes from the tag's `width`/`height` attributes. ### Three-step process ```python # 1. Extract marker context blurbs markers = media.extract_markers(clean_html, processor.attr_store) # 2. Single vision call — one screenshot, all markers listed descriptions = media.describe(screenshot_data_url, clean_html, processor.attr_store, markers) # 3. Splice descriptions in as HTML comments final_html = media.inject_descriptions(clean_html, descriptions) ``` ### Injected comment format ```html
    ``` ### Configuration | Var | Default | Purpose | |---|---|---| | `MEDIA_DESCRIBE_MODEL` | `google/gemini-3.1-flash-lite-preview` | Primary vision model. | | `MEDIA_DESCRIBE_FALLBACK_MODEL` | *(unset)* | If set, 50/50 traffic split for rate-limit relief. | | `MEDIA_DESCRIBE_TIMEOUT` | `60` s | Per-call timeout. | Returns `[]` on any failure — the pipeline continues without descriptions. No `max_tokens` cap: caps produced silent truncation when Gemini looped on a long description and emitted an unterminated JSON array. ## End-to-end flow ``` page loaded + selector resolved │ ▼ extract_element (CDP: outerHTML + computedStyles + interactions) │ ▼ HTMLProcessor.clean_html ├─ block shells: [svg-N], [canvas-N], [script-N] ├─ attr markers: [src-N], [bg-N], [href-N], … ├─ class markers: [class-N] + original classes └─ strip noise │ ▼ keak_ids.inject → data-keak-id on every descendant │ ▼ media.describe → one Gemini Flash Lite vision call │ ▼ media.inject_descriptions → comments │ ▼ visibility.ensure_element_visibility ├─ scroll into view ├─ dismiss banners / overlays (layers 0-2) └─ Flash Lite vision fallback │ (mobile fallback if display:none at desktop) ▼ capture_element_views ├─ dismiss_overlays (page-level: autoconsent + vendor + structural + late-poll) ├─ wait_for_visual_settle (1500ms animation settle budget) ├─ _force_content_visible (content-visibility:auto accordion fix) ├─ _measure (offsetTop/Height/Width) ├─ scroll to offsetTop └─ page.screenshot → _downsize_screenshot (640px wide, ≤1200px tall) │ ▼ ElementPrep returned to pipeline (original.html, original.png, media.json, markers.json, computed_styles.json, …) ``` After the LLM generates a variant, `render.render` runs `ensure_element_visibility` again then `capture_element_views` for the variant screenshot — same contract, same dimensions, so the QA's vision comparison stays apples-to-apples. ## Related - [Pipelines → Stage 0 (Prism)](/docs/keachrome/pipelines#stage-0--preprocess--the-shared-context) — where this prep slots into the Prism funnel. - [Generation flows → Media](/docs/keachrome/generation-flows#media) — KeaChrome describes media, the dashboard generates it. - Repo's [`docs/preservation-system.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/preservation-system.md) — the wider preservation contract that `HTMLProcessor` is one half of. - Repo's [`docs/page-agent-viewport-size.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/page-agent-viewport-size.md) — how callers pin the Playwright viewport for capture. --- /docs/keachrome/request-lifecycle # Request lifecycle Every heavy V2 endpoint (`/v2/variants/{generate,refine}`, `/v2/page-agent/{generate,refine,optimize}`) is **asynchronous**: the request returns a `job_id` immediately, a background thread runs the pipeline, and the client polls for progress and the result. ```mermaid sequenceDiagram actor Client participant API as api/ router participant Store as JobStore participant Sched as JobScheduler participant Runner as pipeline runner Client->>API: POST /v2/variants/generate API->>Store: create(request) → "queued" API->>Sched: submit(job_id, runner) API-->>Client: { job_id, status: "queued" } Note over Sched: dispatcher promotes when caps + memory allow Sched->>Runner: promote → run pipeline Runner->>Store: progress + intermediate variants Client->>API: GET /v2/variants/{job_id} API-->>Client: { status: "running", progress, latest_variant_html } Runner->>Store: set_complete(result) Client->>API: GET /v2/variants/{job_id} API-->>Client: { status: "complete", result } ``` The moving parts: the `api/` routers create + serve, the `JobScheduler` (`src/ops/scheduler.py`) admits + promotes, the `JobStore` (`src/ops/job_store.py`) is the thread-safe record, and the `RSSGovernor` (`src/ops/memory_governor.py`) gates on memory. ## Scheduling gates Two gates guard *admission*, three guard *promotion*: | Stage | Gate | Default | Env var | On failure | |---|---|---|---|---| | Submit | Queue depth | 100 | `MAX_QUEUED_JOBS` | `503 queue_full` | | Submit | Hard memory | 85% | `RSS_HARD_PCT` | `503 memory_pressure` | | Promote | Global cap | 15 | `MAX_CONCURRENT_JOBS` | wait in queue | | Promote | Soft memory | 70% | `RSS_SOFT_PCT` | hold all promotion | | Promote | Per-session cap | 5 | `MAX_CONCURRENT_PER_SESSION` | skip *this* job | **First-eligible, not strict-FIFO.** A job whose session is already at its cap is skipped (not blocked), so one busy session can't starve the rest. ## Job state & live preview `JobStore` is the single source of truth. Statuses: `queued` → `running` → `complete` | `failed` | `cancelled`. A `queued` poll carries `queue_position`; a `running` one streams `progress[]` and a live `latest_variant_html` (see below). ## What a poll returns `GET /v2/variants/{job_id}` (and the page-agent twin) read the record back: ```jsonc { "status": "running", // queued | running | complete | failed | cancelled "progress": [ { "step": "…", "message": "…", "timestamp": "…" } ], "result": { // present once complete (previewed earlier via inject) "variant_html": "…", "title": "…", "change_summary": "…", "reasoning": { "copy": "…", "layout": "…", "visual": "…" }, "confidence": "high", "rounds": 2, "approved": null // /refine sets this so the client knows when to stop }, "versions": [ /* archived prior results */ ], "latest_variant_html": "…", // live preview before completion "rendered_screenshot_url": "…", "queue_position": null, // 0-based while queued "total_cost_usd": 0.0, "log_url": null, "artifact_url": null, "error": null } ``` Page-agent variants add `variant_id`, `session_id`, `resolved_selector`, `original_html`, and `viewport_views`. A few `on_progress(step, …)` steps are interpreted specially so the client sees a **live preview** before completion: | `step` | Effect on the record | |---|---| | `inject` | sets `latest_variant_html`; seeds `result` with the draft | | `inject_meta` | merges `title` / `change_summary` / `reasoning` / `confidence` into `result` | | `rendered_screenshot` | sets `rendered_screenshot_url`, appends to `rendered_screenshots[]` | | `original_screenshot` | sets `original_screenshot_url` | | *anything else* | appended to `progress[]` as `{ step, message, timestamp }` | On finish, `set_complete(result)` archives the prior result into `versions[]`, strips internal-only fields (`_conversation`, `_original_html`, `_cost_usd`, …), and a `finally` attaches `log_url` / `artifact_url` / `total_cost_usd` regardless of outcome. ## Compound jobs (`/optimize`) The `/optimize` parent wires `extract_context → analyze → generate × N`. Its `compound` field holds `page_context`, `page_strategy`, `persona`, a `sections[]` array (each with its own `status`/`job_id`/`result`), and `child_job_ids[]`. It runs with `session_id=None` so it doesn't compete with its own children for the per-session cap. See [Generation flows](/docs/keachrome/generation-flows#the-optimize-compound). ## Cancellation `POST /v2/jobs/{job_id}/cancel` fires two channels: the `JobStore` flag (the pipeline checks `cancel_check()` between LLM calls and bails) and a scheduler interrupt (removes a queued job, or cancels a running job's asyncio task). For a compound `/optimize` job, cancellation **fans out** to every child. The runner's `finally` still finalizes artifacts. ## Job artifacts — the audit black box Every v2 job gets a `JobArtifactStore` (`src/monitoring/dev_artifacts.py`) — always on, not `DEV_MODE`-gated. Artifacts are written to disk as the pipeline runs, so the `finally` block uploads whatever exists before any failure or cancellation. At `finalize()`, every file under `artifacts/{job_id}/` is uploaded to Vercel Blob at `{YYYY-MM-DD}/{job_id}/{relpath}` and the job is registered in the durable `JobIndex` so polls that miss the in-memory `JobStore` after TTL eviction can still locate `response.json`. On-disk layout per job: ``` artifacts/{job_id}/ original.html # raw extracted outerHTML clean.html # after HTMLProcessor (markered) raw_variant.html # LLM output, pre-sanitize sanitized_marker.html # after mechanical QA final.html # after restore() — what the client receives stage0a-persona.json # per-stage pipeline snapshots stage1-critique-visitor.json … metadata.json screenshot-{stage}.png run.log # per-job log (always written via attach_log_handler) screenshots.json # all blob URLs recorded during this job decisions.json # structured branch log (scope, cache, fallback, etc.) response.json # exact response envelope shipped to the client refs.json # session-tier blob + parent job (for /optimize children) _meta.json # pipeline, status, timestamps, models, costs llm/ 01-{role}/ response.txt # raw LLM response, untruncated _meta.json # model, tokens, cost, duration, messages_sha256, template hint messages.json # rendered prompt — only when CAPTURE_LLM_PROMPTS=1 ``` Local folder is removed after upload **unless `DEV_MODE=1`**. Per-job log isolation comes from `attach_log_handler` — a `FileHandler` on `run.log` whose `_JobIdFilter` reads the `current_job_id` ContextVar, so concurrent jobs don't cross-contaminate. | Var | Default | Effect | |---|---|---| | `DEV_MODE` | `0` | `1` → leave local folder after upload. | | `JOB_ARTIFACTS_ROOT` | `/artifacts` | Override the local artifact root. | | `CAPTURE_LLM_PROMPTS` | `0` | `1` → also write `llm/{NN}/messages.json`. | | `BLOB_READ_WRITE_TOKEN` | *(required for upload)* | Vercel Blob token. | ## Deeper Concurrency caps, the queue, the RSS governor, and the Stage 0/1 caches are detailed in the repo's [`docs/job-scheduling-and-caches.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/job-scheduling-and-caches.md). Full screenshot/HTML-prep / artifact-store coverage lives in [`docs/screenshot-pipeline.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/screenshot-pipeline.md) (mirrored at [Element prep & screenshots](/docs/keachrome/preparation)). --- /docs/keachrome/templates # Template representation How a variant is encoded as a **portable diff** so the pixel can replay it on other pages that share a template. This is what `POST /v2/templatify` returns and what the pixel's applier consumes. Engine: `src/utils/html/templatify.py` (doc compiler) + `src/utils/html/apply_gate.py` (gate). For *why* it exists, see [PDP templates](/docs/keachrome/generation-flows#pdp-templates-templatify); for the end-to-end product flow (where templates are compiled, stored, delivered, and enforced), see [Run on similar pages](/docs/platform/similar-pages). **The problem it solves:** a PDP variant is generated against one product (A) and carries A's title/price/copy mixed with the design change. Replacing product B's section with A's HTML would overwrite B's content. Templatify separates the two — **design ports, content stays.** And because B is found by a CSS selector that can collide with unrelated elements (a landing-page hero matching the PDP's positional selector), the template ships with a **gate** that decides per element whether it may apply at all. ## One template, two encodings The compiler emits one; the applier consumes both. A `list` is **`ops`** (legacy); a `dict` is **`morph`** — the production format. | Encoding | Shape | Selectors | Extras | |---|---|---|---| | `ops` | ordered list of op dicts | native CSS `:scope > TAG:nth-of-type(N) > …` | — | | `morph` (v2) | one selector-keyed doc | anchored compact `tag:N@cls.cls>…` | ships with the apply **gate**; ~58% smaller than ops on a real PDP | Compact selectors are not valid CSS — they need the morph resolver (below), not `querySelector`. ## The `ops` vocabulary Design-only by construction. There is **no content op** (`replace_text` / `set_innerHTML`) — that's the whole point. | `op` | fields | effect | |---|---|---| | `prepend_style_block` | `css` | insert `` as root's first child | | `add_class` | `selector, classes:[…]` | append missing classes | | `remove_class` | `selector, classes:[…]` | drop listed classes | | `set_styles` | `selector, styles:{p:v}` | merge into inline style (**delta**) | | `set_attrs` | `selector, attrs:{k:v}` | set attrs (never `class`/`style`) | | `insert_after` / `insert_before` | `selector, html, moves?` | insert siblings at the anchor (slotted when `moves` present) | | `insert_at_start` | `selector(=parent), html, moves?` | insert as parent's first children | | `remove` | `selector` | delete node | | `wrap` | `selector, tag?, attrs?` | wrap node (apply-supported; not emitted) | `set_text` and content-bearing `remove`s exist only in the **raw** compile (`preserve_content=false`); the template filter drops them. ## The `morph` doc (v2) ```jsonc { "v": 2, "style": "…css…", // optional, ⇒ one as root's first child 2. set : for each (selector → patch), in doc order: class → node.class = value.split() # wholesale (design) +class → append missing classes # additive styles → merge into inline style # delta (content-safe) attrs → set each attribute text → replace children with a text node # global mode only 3. rm : decompose each resolved node # rm is a set 4. ins : resolve anchor AND all mv nodes first; fill s by MOVING the (already patched) mv nodes in; insert at after/before/start. Any unresolved move ⇒ skip the whole insert. ``` Patches run before moves so a moved node carries its design changes into the new wrapper. The insertion point is captured before moves detach nodes (the anchor's neighbour may itself be a move target). ## Region self-verification (compile time) After compiling and filtering, the compiler **replays its own ops on A** and compares the result against the variant, region by region (per top-level child of the element), using a text-insensitive canonical form. Tolerated mismatches are text-bearing extras on either side — target-preserved content (B's own copy) and unported variant copy are both expected. Any region that does not verify is **dropped wholesale**, including regions coupled to it through `mv` references. The doc that ships is therefore structurally guaranteed to reproduce the variant's design on A — not just likely to. ## Invariants - **`class` = design** (wholesale swap OK); **`style` = content** (delta only — inline style can carry a swatch color or hero image, which must not leak A→B). - **`+class` is additive** so the target keeps its own utility classes. - **Content never crosses** in template mode: `set_text` dropped, attrs filtered to a design allowlist (`role`, `type`, `aria-*`, `data-state`, …), content-bearing style *values* (`url(...)`, A-specific tokens/prices) scrubbed, content-bearing removes skipped, restructures expressed as slotted moves of the target's own nodes rather than copies. - The vocabulary is **complete in principle** (any unaligned node → `remove` + whole `insert`), but template mode is **biased to safety, not completeness** — it drops a design change rather than risk a content leak. Region self-verification enforces this at the region level. ## Endpoint `POST /v2/templatify` — synchronous, pure-CPU, Bearer-auth. ```jsonc // request { "original_html": "
    A's element
    ", "variant_html": "
    A's element, redesigned
    ", "preserve_content": true, // true ⇒ cross-product template (PDP); false ⇒ all ops (nav/footer) "format": "morph" // "ops" (default) | "morph" } // response (format=morph) { "ops": { "v": 2, /* morph doc */ }, "gate": { /* apply-gate profile */ }, "op_count": 12, "raw_op_count": 31, "preserve_content": true, "format": "morph" } ``` Blank input → `400`; compile/format error → `422`. ## Consumers and conformance The dashboard pixel re-implements the gate arithmetic and doc replay in JS (`dashboard-no-code-v3/lib/scriptModules/templateApplier.js`), and the Chrome extension ports the gate in TS (`react-panel/src/lib/templateGate.ts`) to scope its active-test badges. All three are **conformance-tested equal**: the KeaChrome benchmark (`benchmarks/templatify/`) generates reference artifacts (`template_doc.json`, Python replay output), and the dashboard's `tests/dom/template-applier.spec.ts` asserts JS ≡ Python on gate verdicts and replay output, and extension ≡ pixel on gate verdicts. **Change them together.** The full formal spec lives in the repo's [`docs/template-representation.md`](https://github.com/keak-ai/KeaChrome-API/blob/main/docs/template-representation.md). --- /docs/mcp/ # Keak MCP An MCP server that connects an MCP client (Claude Code, Claude Desktop, claude.ai) to a Keak account. Each install is bound to **one** Keak user via that user's personal API key — so "what are my active tests?" returns *your* tests, and "optimize my pricing page" runs against *your* account. It is the conversational front door to Keak's **hands-off CRO** loop: discover tests and domains, recommend conversion goals, optimize a page into ready-to-launch variants, create tests from those variants, and toggle autopilot. It is a **thin client** — every tool maps to one dashboard endpoint; the dashboard owns auth, billing, persistence, and the AI work. > **Repo:** [github.com/keak-ai/Keak-MCP](https://github.com/keak-ai/Keak-MCP) ``` MCP client (Claude) ──tool call──▶ Keak MCP ──HTTPS + keak_live_ key──▶ Dashboard API (→ KeaChrome) ``` ## A typical conversation ``` You: Recommend conversion goals for https://mysite.com/pricing Keak: goal_1 [cta] "More checkouts" … goal_2 [page_view] … (cached) You: Optimize that page for goal 1 Keak: report_1 — generating… (call get_cro_report to advance) You: How's it going? Keak: report_1 complete — section 1 [hero] "Bolder value prop" … Share the report: https://platform.keak.com/preview/abc123 You: Launch section 1 as a test on domain 42 Keak: Test created and live. test_987 … You: Turn on autopilot for it Keak: Autovariation ENABLED for test_987. ``` The CRO report is the **same flow new signups get** — it produces a shareable `/preview/{token}` link. Opaque goal targets and generated HTML stay in the server's session, so you reference goals/sections by short handles (`goal_2`, `report_1`, `section 2`) instead of re-pasting payloads. ## Install Get a key from the Keak dashboard → **Integrations → API Keys → Create API Key** (looks like `keak_live_…`). Build the server (`npm install && npm run build`), then: **Claude Code:** ```bash claude mcp add keak --env KEAK_API_KEY=keak_live_... -- node /absolute/path/to/Keak-MCP/dist/index.js ``` **Claude Desktop** — add to `claude_desktop_config.json`: ```json { "mcpServers": { "keak": { "command": "node", "args": ["/absolute/path/to/Keak-MCP/dist/index.js"], "env": { "KEAK_API_KEY": "keak_live_..." } } } } ``` Tools load at client startup — start a **new** session after installing. | Env var | Required | Default | Notes | |---|---|---|---| | `KEAK_API_KEY` | yes | — | Your `keak_live_…` key | | `KEAK_API_BASE_URL` | no | `https://platform.keak.com` | Set `http://localhost:3000` for local dev | ## Read next - **[Tools reference](/docs/mcp/tools)** — every tool, its args, and what it does. - **[Architecture](/docs/mcp/architecture)** — layers, session memory, async polling, adding a tool. - **[Dashboard endpoints](/docs/mcp/endpoints)** — how each tool maps to the [Platform API](/docs/platform/api) and how it authenticates. --- /docs/mcp/architecture # Architecture The MCP is the interface between users and Keak's (constantly evolving) services, so it's built to be **cheap to extend**: adding a capability is one new file plus one line. ## Layers ``` index.ts entrypoint — stdio transport └ server.ts assembly — builds McpServer, injects context, registers tools, formats thrown errors once (tools never try/catch transport) ├ config.ts env → { apiKey, baseUrl } ├ keak-client the ONLY place fetch lives. Typed /api/v1 methods, unwraps the │ { data } envelope, throws KeakApiError on failure. ├ context/ SessionStore — per-session memory (see below) ├ lib/ │ tool.ts defineTool() contract + text()/errorResult() helpers │ format.ts presentation: compact summaries + stable handles │ poll.ts bounded async-job poller ├ types.ts shared API shapes └ tools/ one capability per file; tools/index.ts is the registry ``` **Dependency rule:** tools depend on `lib`, `keak-client`, `context`, `types` — never on each other. Shared logic goes in `lib` or `keak-client`. ## Session memory A stdio MCP server is spawned once per client session and lives for it, so the in-memory `SessionStore` is naturally session-scoped. It holds the things users refer back to but that are awkward to round-trip through the model: - **Recommended goals** — referenced later by number (`goal_2`). The opaque `conversion_targets` blob lives here; the model only needs the index. - **`client_session_id` per page** — reused across recommend-goals → optimize so the backend shares its page-bootstrap cache. - **CRO reports** — full variant HTML kept under a handle (`report_1`), so `create_test(from_report, section)` launches a test without the HTML ever passing back through the model. ## Display strategy - Lists/details → compact, one line per item, with stable handles (`test_123`, `goal_2`, `report_1` / `section 2`) the model can quote next turn. - Large payloads (variant HTML) are **never** dumped by default — responses show a size + summary; the full HTML is fetched only via `get_cro_report(section=N)`. ## Async jobs MCP clients enforce a per-request timeout (~60s in the SDK). CRO reports take minutes, so `create_cro_report` starts the job and polls only up to ~45s, then returns a handle. The model advances the job with repeated `get_cro_report` calls. The polling budget is capped at 55s to stay under the client limit (`lib/poll.ts`). ## Design note: page-agent, not a v1 wrapper The hands-off tools hit the **real** page-agent surface, not a parallel v1 layer: - `recommend_conversion_goals` → `/api/v2/page-agent/recommend-goals` - `create_cro_report` / `get_cro_report` → `/api/v2/audits` (+ `/:id`) — the **same shareable-report pipeline new signups get**, so completion mints the identical `/preview/{token}` link. - Test/domain CRUD stays on `/api/v1` (the public REST surface). Those v2 routes accept either a Supabase JWT *or* a `keak_live_` key (`withAuthOrApiKey`); adding the key path was additive and left dashboard/extension JWT callers unchanged. Two response envelopes result (`/api/v1` → `{ data }`; `/api/v2` → raw), which `keak-client` handles. Full map: [Dashboard endpoints](/docs/mcp/endpoints). ## Adding a tool 1. Create `src/tools/my-thing.ts` with `defineTool({ name, title, description, inputSchema, handler })`. The handler receives `{ client, session }` and returns a compact, handle-bearing `text(...)`. 2. If it hits a new endpoint, add a typed method to `keak-client.ts` (and a shape to `types.ts`). **Never call `fetch` from a tool.** 3. Register it: import + add to the array in `src/tools/index.ts`. Errors: throw (the client throws `KeakApiError`) and let `server.ts` format it once, or return `errorResult("…")` for validation messages. Relative imports use `.js` extensions (tsconfig `module: Node16`). --- /docs/mcp/endpoints # Dashboard endpoints Which dashboard endpoint each MCP tool calls, and how it authenticates. The MCP is a thin client — every tool maps to one [Platform API](/docs/platform/api) endpoint. It always sends `Authorization: Bearer ` (a `keak_live_…` key). ## Auth modes | Wrapper (dashboard) | Accepts | Used by | |---|---|---| | `withApiKeyAuth` | **`keak_live_` key only** | the public REST surface (`/api/v1/*`) | | `withAuthOrApiKey` | **Supabase JWT *or* `keak_live_` key** | the page-agent + report surface (`/api/v2/page-agent/recommend-goals`, `/api/v2/audits`) | Dual-auth endpoints also serve the dashboard/extension (JWT); adding the key path was additive and left JWT callers unchanged. ## Discovery / monitoring — `/api/v1` (public REST) | Tool | Method & path | Response | Notes | |---|---|---|---| | `list_domains` | `GET /api/v1/domains` | `{ data[], pagination }` | numeric `domain_id` for create\_test | | `list_tests` | `GET /api/v1/tests?status=&limit=&offset=` | `{ data[], pagination }` | scoped to the key owner's domains | | `get_test` | `GET /api/v1/tests/:id` | `{ data }` | id like `test_123` | | `get_test_results` | `GET /api/v1/tests/:id/results?date_from=&date_to=` | `{ data }` | improvement, confidence, per-variant | ## Hands-off CRO — `/api/v2` (page-agent + report flow) | Tool | Method & path | Response | Notes | |---|---|---|---| | `recommend_conversion_goals` | `POST /api/v2/page-agent/recommend-goals` | raw `{ suggestions[], cost_usd }` | the real page-agent recommender; MCP mints `client_session_id` | | `create_cro_report` | `POST /api/v2/audits` | raw `{ audit_id, token, status, report_url }` | starts the hands-off-cro report (flow `api`); runs page-agent `/optimize` under the hood; no Slack/email | | `get_cro_report` | `GET /api/v2/audits/:id` | raw `{ status, sections[], preview_url, … }` | advances the job one step per call; `preview_url` (`/preview/{token}`) mints on completion | `create_cro_report` → `startSelfServeCroReport` writes a `shareable_reports` row owned by the key user and submits the page-agent optimize compound. `get_cro_report` → `advanceAuditFromUpstream` + `finalizeCompletedAudit` — the same pipeline new-signup CRO reports use, so the `/preview/{token}` link is identical. See [Hands-off reports](/docs/platform/reports). ## Launch / control — `/api/v1` (public REST) | Tool | Method & path | Notes | |---|---|---| | `create_test` | `POST /api/v1/tests` | `from_report` pulls selector + control + variant from a cached report section | | `update_test` | `PATCH /api/v1/tests/:id` | rename / pause / resume / url / min views | | `set_autovariation` | `PATCH /api/v1/tests/:id/autovariation` | explicit `{ enabled }` on/off | ## Not used by the MCP (deliberately) - `/api/admin/audits/*` — admin-only (`withAdminAuth`); the MCP uses the API-key-authed `/api/v2/audits` twin instead. - Internal JWT-only routes (`/api/tests`, `/api/v2/page-agent/{generate,refine, extract_context,…}`) — not exposed yet. `withAuthOrApiKey` makes any of them one-line dual-authable when a tool needs them. - KeaChrome is never called directly — only through the dashboard, which owns per-user scoping, persistence, and billing. ## Response envelopes (why the client has two) - `/api/v1/*` → `{ data }` / `{ data, pagination }` (the public REST envelope). - `/api/v2/page-agent/*` and `/api/v2/audits/*` → **raw** JSON (no envelope). ```jsonc // v1 (enveloped) — GET /api/v1/tests { "data": [ { "id": "test_123", "name": "…", "status": "active", "metrics": { "impressions": 5120, "conversions": 240, "conversion_rate": 0.0469 } } ], "pagination": { "total": 7, "limit": 20, "offset": 0 } } // v2 (raw) — POST /api/v2/audits { "audit_id": "aud_…", "token": "abc123", "status": "queued", "report_url": "https://platform.keak.com/audit/abc123" } ``` `keak-client.ts` unwraps `.data` for v1 methods and returns the raw payload for v2. Errors: v1 → `{ error: { code, message, field? } }`; v2 → `{ error: "…" }` / `{ detail }`. Full request/response shapes for every field: [Platform → Public API](/docs/platform/api#request--response-shapes). --- /docs/mcp/tools # Tools reference The capabilities the MCP exposes to the model. Each tool is one file under `src/tools/`, registered in `src/tools/index.ts`. For the exact dashboard endpoint and auth behind each, see [Dashboard endpoints](/docs/mcp/endpoints). ## Discovery & monitoring | Tool | Args | Does | |---|---|---| | `list_domains` | `limit?`, `offset?` | List your domains + numeric `domain_id` (needed to create tests). | | `list_tests` | `status?` (active/paused/completed), `limit?`, `offset?` | List your A/B tests + metrics. | | `get_test` | `test_id` | One test's details + metrics. | | `get_test_results` | `test_id`, `date_from?`, `date_to?` | Improvement, confidence, per-variant stats. | ## Hands-off CRO | Tool | Args | Does | |---|---|---| | `recommend_conversion_goals` | `page_url` | 3–5 suggested goals (`goal_1…`), cached for reuse. | | `create_cro_report` | `page_url`, `goal_number?` / `goal_title?`, `scope?`, `max_sections?`, … | Run hands-off CRO; returns `report_N` + per-section summaries + a shareable `/preview` link. | | `get_cro_report` | `report_id?`, `section?`, `wait?` | Advance/refresh a report, or fetch a section's full variant HTML. | ## Launch & control | Tool | Args | Does | |---|---|---| | `create_test` | `domain_id` + (`from_report`+`section`) or explicit fields | Launch an A/B test (e.g. straight from a report section). | | `update_test` | `test_id`, `name?`, `status?`, `url?`, `minimum_view_count?` | Rename / pause / resume / edit. | | `set_autovariation` | `test_id`, `enabled` | Turn autopilot on/off (auto-generate + roll out winners). | ## Input schemas (the two rich tools) Each tool's input is a Zod schema (`src/tools/*.ts`); `?` = optional. The two below carry the only non-obvious shapes. **`create_cro_report`** — goal resolution priority: explicit `conversion_targets` > `goal_title` > `goal_number` (against the last `recommend_conversion_goals`); none ⇒ whole page. | Field | Type | Notes | |---|---|---| | `page_url` | `string` (url) | **Required.** Page to optimize. | | `goal_number` | `int ≥ 1`? | Use the Nth recommended goal. | | `goal_title` | `string`? | Match a recommended goal by title substring. | | `conversion_targets` | `string`? | Advanced: opaque target string (overrides the goal fields). | | `scope` | `"COPY"\|"DESIGN"`? | Limit changes to copy- or design-only. | | `max_sections` | `int 1–7`? | Max sections to optimize. | | `custom_prompt` | `string ≤4000`? | Extra steering for the optimizer. | | `input_images` | `string[] (url) ≤3`? | Reference image URLs forwarded to every section's generation as visual context (a design to emulate, a brand reference). Oversized images are downsized dashboard-side. | | `timeout_seconds` | `int 5–55`? | Poll budget before handing back a handle (default 45; capped at 55 — the client timeout). | **`create_test`** — two modes; `domain_id` is always required. | Field | Type | Notes | |---|---|---| | `domain_id` | `int > 0` | **Required** (see `list_domains`). | | `from_report` + `section` | `string` + `int ≥ 1` | **Mode A:** pull selector + control + variant from a cached report section — HTML never round-trips the model. | | `url`, `element_selector`, `control`, `variant` | `string` | **Mode B:** supply them explicitly. | | `type` | `"text"\|"headline"\|"CTA"\|"copy"\|"image"\|"video"\|"html"`? | Defaults to `html` for report-sourced tests. | | `name` | `string ≤255`? | Defaults from the section title. | | `minimum_view_count` | `int ≥ 100`? | Min views before a decision. | ## What a call returns Tools return **compact text**, not raw JSON — a summary plus stable handles to quote next turn (the full HTML/goal blobs stay in [session memory](/docs/mcp/architecture#session-memory)): ```text create_cro_report → report_1 — generating (4 sections) 1 [hero] "Bolder value prop" complete 2 [pricing] "Anchored 3-tier" generating Share: https://platform.keak.com/preview/abc123 Still running after 45s. Call get_cro_report(report_id="report_1", wait=true) to keep waiting. get_cro_report(section=1) → the full variant_html for section 1 (explicit opt-in) create_test(from_report="report_1", section=1, domain_id=42) → Test created and live. test_987 … ``` ## Handles, not payloads Goals and report sections are referenced by short, stable handles (`goal_2`, `report_1`, `section 3`) that the model can quote on the next turn. The opaque `conversion_targets` blob and the generated variant HTML live in the server's [session memory](/docs/mcp/architecture#session-memory) — never round-tripped through the model. Large payloads (variant HTML) are summarized by default and fetched in full only via `get_cro_report(section=N)`. ## Verify without an MCP client ```bash printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_tests","arguments":{"limit":5}}}' \ | KEAK_API_KEY="keak_live_..." node dist/index.js 2>/dev/null ``` The `id:2` response lists your active tests. --- /docs/platform/ # Platform (Dashboard) The Next.js app at `platform.keak.com`. It's the layer that turns KeaChrome's raw AI generation into a **product**: it owns users, domains, tests, and persistence; wraps every call with **auth + billing**; runs the **A/B testing + autovariation loop**; and serves the hands-off **audit/report** flow. The extension and the [Keak MCP](/docs/mcp/) are clients of *this* app — not of KeaChrome directly. > **Repo:** [github.com/keak-ai/dashboard-no-code-v3](https://github.com/keak-ai/dashboard-no-code-v3) ## What it owns | Concern | Notes | |---|---| | **Auth & users** | Supabase JWT (dashboard/extension) + `keak_live_` API keys. Domain team roles. | | **Domains & tests** | Test/variation/analytics model + lifecycle (`prisma/schema.prisma`). | | **Billing** | Pay-as-you-go impressions, credits, POC links, entitlements gate. See [Billing & Ops](/docs/billing/overview). | | **The testing loop** | Impressions/conversions → SPRT decision → autovariation. See [The testing loop](/docs/platform/testing-loop). | | **Hands-off reports** | Audit pipeline, `/preview` split-screen, 👍/👎 feedback, Slack. See [Hands-off reports](/docs/platform/reports). | | **AI proxy** | Calls KeaChrome at `CRO_BACKEND_API_URL` (key `CRO_BACKEND_API_KEY`), adding per-user scoping + billing. | ## Auth wrappers Every route handler is wrapped by one guard (`app/api/lib/{auth,v1-auth,admin-auth}.ts`): | Wrapper | Accepts | Used by | |---|---|---| | `withAuth` | Supabase JWT | internal dashboard/extension routes | | `withAuthOrApiKey` | JWT **or** `keak_live_` key | `/api/v2/*` (page-agent + audits) — shared by dashboard and API clients | | `withApiKeyAuth` | `keak_live_` key only | `/api/v1/*` (public REST, the MCP) | | `withAdminAuth` | admin JWT | `/api/admin/*` (ops only) | | `withServiceAuth` | service key | cross-service / webhook routes | ## How it talks to KeaChrome KeaChrome is never exposed publicly. The dashboard proxies `/api/v2/page-agent/*` and the audit flow to `CRO_BACKEND_API_URL`, swapping the caller's JWT/API-key for the backend service key and enforcing ownership + billing first. Generation is asynchronous on both sides: the dashboard submits a KeaChrome job, stamps the `kea_job_id`, and advances it by polling. ## Scheduled work (crons) `vercel.json` drives the time-based half of the platform: | Cron | Schedule | Purpose | |---|---|---| | `process-variation-jobs` | every even minute | the autovariation loop — generate the next variant | | `process-snapshots` | every odd minute | recompute SPRT snapshots for the dashboard | | `reconcile-audits` | every 2 min | advance in-flight CRO reports | | `rollover-usage` | daily | reset usage-period counters | | `meter-sync` | hourly | drain credits, report impressions to Stripe | | `payment-failures` / `process-downgrades` / `credit-expiry` | 15 min / daily / daily | billing lifecycle (see [Changelog](/docs/billing/changelog)) | ## Configuration The env vars that wire the platform together. Full list (with staging↔prod Supabase switching) in the repo's [`docs/environment-setup.md`](https://github.com/keak-ai/dashboard-no-code-v3/blob/main/docs/environment-setup.md). | Group | Vars | Notes | |---|---|---| | **App** | `NEXT_PUBLIC_APP_URL` | Base URL for script URLs, cron callbacks, email links. | | **Supabase** | `NEXT_PUBLIC_SUPABASE_URL`, `…_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `SUPABASE_JWT_SECRET`, `SUPABASE_DATABASE_URL`, `SUPABASE_DIRECT_URL` | Auth + Postgres. Prisma reads `SUPABASE_DATABASE_URL` (not `DATABASE_URL`). Staging/prod are **separate projects** — never mix credentials. | | **KeaChrome proxy** | `CRO_BACKEND_API_URL`, `CRO_BACKEND_API_KEY` | Where + how the dashboard calls the [AI engine](/docs/keachrome/). | | **Cross-service** | `INTERNAL_API_SECRET`, `SERVICE_API_KEY` | `INTERNAL_API_SECRET` must match KeaChrome's exactly (error hub). `SERVICE_API_KEY` ↔ script-api. | | **Billing** | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_*_PRICE_ID` | See [Billing & Ops](/docs/billing/overview). | | **Crons** | `CRON_SECRET` | Authenticates the scheduled self-callbacks below. | | **Email / analytics / blob** | `RESEND_API_KEY`, `NEXT_PUBLIC_POSTHOG_*`, `BLOB_READ_WRITE_TOKEN` | Transactional email, product analytics, script caching. | ## Read next - **[Public API](/docs/platform/api)** — `/api/v1` + `/api/v2`, the surface the MCP and extension call. - **[The testing loop](/docs/platform/testing-loop)** — test model, SPRT, autovariation, media variants. - **[Hands-off reports](/docs/platform/reports)** — audit pipeline, preview, feedback, Slack. --- /docs/platform/api # Public API The surface external clients call: the [Keak MCP](/docs/mcp/), the extension, and any customer integration. Two families, two auth modes, two response envelopes. | Family | Auth | Envelope | Handlers | |---|---|---|---| | `/api/v1/*` | `withApiKeyAuth` (`keak_live_` key) | `{ data }` / `{ data, pagination }` | `app/api/v1/**/route.ts` | | `/api/v2/*` | `withAuthOrApiKey` (JWT **or** key) | **raw** JSON | `app/api/v2/**/route.ts` | Every request carries `Authorization: Bearer ` — a `keak_live_…` key for `/api/v1` (and either a key or a Supabase JWT for `/api/v2`). ## `/api/v1` — public REST | Method | Path | Purpose | |---|---|---| | `GET` | `/api/v1/domains` | List accessible domains + numeric `domain_id`. | | `POST` | `/api/v1/tests` | Create a test (`domain_id`, `name`, `url`, `type`, selector, control, variant, …). | | `GET` | `/api/v1/tests` | List tests; filter `?status=active\|paused\|completed`. | | `GET` | `/api/v1/tests/:id` | Test details + metrics. | | `PATCH` | `/api/v1/tests/:id` | Update `name` / `status` / `url` / `minimum_view_count`. | | `DELETE` | `/api/v1/tests/:id` | Delete a test (domain admin). | | `GET` | `/api/v1/tests/:id/results` | Improvement, confidence, per-variant stats (`?date_from=&date_to=`). | | `PATCH` | `/api/v1/tests/:id/autovariation` | Toggle autopilot `{ enabled }` (idempotent). | | `POST` | `/api/v1/conversions` | Record a conversion. | | `GET` | `/api/v1/conversions` | List conversions (`?test_id=`). | ## `/api/v2` — page-agent + audits Dual-authed; these proxy KeaChrome and run the hands-off report pipeline. | Method | Path | Purpose | |---|---|---| | `POST` | `/api/v2/page-agent/recommend-goals` | 3–5 suggested conversion goals for a page. | | `POST` | `/api/v2/page-agent/optimize` | Submit the whole-page CRO compound to KeaChrome. | | `GET` | `/api/v2/page-agent/optimize/:jobId` | Poll the optimize job. | | `POST` | `/api/v2/page-agent/optimize/:jobId/cancel` | Cancel it. | | `POST` | `/api/v2/audits` | Start a self-serve CRO report (owns persistence + `/preview`). | | `GET` | `/api/v2/audits/:id` | Advance/poll a report; mints `/preview/{token}` on completion. | | `POST` `GET` | `/api/v2/feedback` | Record / read 👍/👎 on a variant. | `/api/v2/audits` and `recommend-goals` are the routes the MCP uses; the page-agent `generate`/`refine`/`extract_context` routes stay JWT-only for now. The full tool→endpoint map is in [MCP → Dashboard endpoints](/docs/mcp/endpoints); the report internals are in [Hands-off reports](/docs/platform/reports). ### Durable job registry `/api/v2/page-agent/jobs` (POST/GET/PATCH/DELETE) is server-side tracking of in-flight KeaChrome jobs (`PageAgentJob` table) so a job survives a browser/extension restart and is resolvable from any client. The `/optimize` proxy upserts a row on creation; clients mirror the terminal status back with a PATCH after polling upstream. ## Request & response shapes `/api/v1` always wraps the payload (`{ data }`, or `{ data, pagination }` for lists); `/api/v2` returns the upstream payload **raw**. The shapes below are the dashboard serializers (mirrored in the MCP's `types.ts`). **`/api/v1` — tests & domains** ```jsonc // POST /api/v1/tests (create) { "domain_id": 42, "name": "Hero CTA test", "url": "https://site.com/", "type": "page_agent", "element_selector": "#hero", "control": "
    ", "variant": "
    ", "minimum_view_count": 1000 } // → { "data": { "id": "test_123", "name": "Hero CTA test", "status": "active", // "type": "page_agent", "url": "https://site.com/", "created_at": "2026-…", // "metrics": { "impressions": 0, "conversions": 0, "conversion_rate": 0 } } } // GET /api/v1/tests?status=active&limit=20&offset=0 // → { "data": [ { /* Test */ } ], "pagination": { "total": 7, "limit": 20, "offset": 0 } } // PATCH /api/v1/tests/:id { "status": "paused" } → { "data": { /* Test */ } } // PATCH /api/v1/tests/:id/autovariation { "enabled": true } → { "data": { /* Test */ } } // GET /api/v1/domains // → { "data": [ { "id": 42, "name": "Acme", "url": "https://site.com", "created_at": "…" } ], "pagination": {…} } ``` ```jsonc // GET /api/v1/tests/:id/results?date_from=&date_to= { "data": { "test_id": "test_123", "improvement": 0.14, "confidence": 0.97, "variants": { "control": { "impressions": 5120, "conversions": 240, "conversion_rate": 0.0469 }, "variation": { "impressions": 5090, "conversions": 272, "conversion_rate": 0.0534 } }, "totals": { "impressions": 10210, "conversions": 512 }, "date_from": null, "date_to": null } } ``` **`/api/v2` — recommend-goals & audits (raw, no envelope)** ```jsonc // POST /api/v2/page-agent/recommend-goals { "page_url": "https://site.com/pricing" } { "suggestions": [ { "title": "More checkouts", "description": "…", "kind": "cta", "conversion_targets": "[{\"selector\":\"#buy\"…}]" } ], // conversion_targets is OPAQUE — pass verbatim "cost_usd": 0.012 } // POST /api/v2/audits { "page_url": "…", "conversion_targets"?: "…", "scope"?: "COPY"|"DESIGN", "max_sections"?: 5, "input_images"?: ["https://…"] } // input_images: ≤3 reference image URLs forwarded to the optimizer; oversized images are downsized + re-hosted first. { "audit_id": "aud_…", "token": "abc123", "status": "queued", "report_url": "https://platform.keak.com/audit/abc123" } // GET /api/v2/audits/:id (each call advances the job one step) { "audit_id": "aud_…", "token": "abc123", "status": "generating", "sections": [ { "section_type": "hero", "status": "complete", "title": "…", "selector": "#hero", "variant_html": "…", "original_html": "…", "change_summary": "…", "reasoning": {…} } ], "sections_count": 4, "preview_url": null, // /preview/{token}, minted on completion "report_url": "https://platform.keak.com/audit/abc123", "total_cost_usd": 1.6 } ``` ## Errors | Family | Shape | |---|---| | `/api/v1` | `{ "error": { "code": "invalid_request", "message": "…", "field"?: "…" } }` | | `/api/v2` | `{ "error": "…" }` or `{ "detail": … }` (the upstream KeaChrome shape) | Status codes follow HTTP: `400` invalid, `401` unauthorized, `403` forbidden (wrong owner), `404` not found, `409` conflict, `429` rate-limited, `503` back-pressure (`Retry-After`). > Admin-only routes live under `/api/admin/*` (`withAdminAuth`) — e.g. the billing > reconcile and POC-link tools documented in [Billing & Ops](/docs/billing/reconciliation). --- /docs/platform/reports # Hands-off reports The "URL in, shareable CRO report out" flow. A signup, a sales rep, or the [MCP](/docs/mcp/) submits a page; the platform runs KeaChrome's whole-page `/optimize` compound, captures screenshots, and mints a public **split-screen preview** link. All three entry points share one pipeline — they differ only in who owns the row and whether Slack/email fire. ```mermaid flowchart LR S[start*CroReport] --> K[KeaChrome /optimize
    job submitted] K --> A[advanceAuditFromUpstream
    poll each tick] A --> F[finalizeCompletedAudit] F --> P[/preview/{token}
    split-screen + 👍/👎/] ``` ## The pipeline `lib/audit/audit-pipeline.ts` writes a `shareable_reports` row (its `report_data` holds `status`, `sections[]`, the `kea_job_id`, and the snapshot token), then submits the page-agent optimize compound: | Starter | Flow | Owner | Slack / email | |---|---|---|---| | `startSelfServeCroReport` | `api` | the key/user | **none** (silent — used by the MCP) | | `startSalesCroReport` | `sales` | admin | posts to a sales thread | | `startAuditJob` | `admin` | admin | operator email on completion | - **`advanceAuditFromUpstream(auditId)`** — one poll per call (driven by the `reconcile-audits` cron, every 2 min); on `complete` it calls… - **`finalizeCompletedAudit(...)`** — enqueues snapshot captures, mints the `SnapshotShare`, and (for sales/admin flows) posts Slack + sends email. Email gating is centralized so the `api` and `sales` flows stay quiet while admin/legacy flows notify. ## The /preview split-screen `app/preview/[token]/page.tsx` (+ `preview-client.tsx`) resolves a `SnapshotShare` token to baseline + per-variant snapshots and renders them in an **inert iframe** (scripts stripped) with a Keak banner, a device toggle (desktop/tablet/mobile, `?device=`), and a variant rail (`?v=1,3,5`). An `opengraph-image` makes the link unfurl in Slack. Snapshots are produced by `lib/snapshots/snapshot-service.ts` (`enqueueSnapshotVersion`, `createSnapshotShareFromCompletedJob`). ## Feedback (👍 / 👎) `POST` / `GET /api/v2/feedback` records and reads a viewer's rating on a variant (`lib/feedback/variant-feedback-service.ts`, `recordVariantFeedback`). The actor is a fingerprint, so no auth is needed on the preview page; `notify_slack: true` mirrors the rating + comment to the support channel (`lib/support/slack.ts`). ## Slack Report and POC-link notifications post via `lib/crm/slack-sales-channel.ts` and `lib/crm/slack-manual-audit.ts`; variant feedback via `lib/support/slack.ts`. Slack failures never block the underlying job. --- /docs/platform/similar-pages # Run on similar pages (multi-page tests) How a variant authored on one page (typically a PDP) runs on structurally-similar pages — without overwriting each page's own content and without leaking onto unrelated pages whose elements happen to match the same CSS selector. This is the cross-repo design; the template encoding itself is in [Template representation](/docs/keachrome/templates). **Repos involved:** KeaChrome compiles the template (`POST /v2/templatify`, `format="morph"`), the dashboard stores and delivers it and its pixel enforces it, the Chrome extension mirrors the same scoping for its active-test overlays. ## The scoping contract One rule, implemented identically in the pixel (`domManipulator.js` + `templateApplier.js`) and the extension (`templateGate.ts` + `activeTestsStore.ts`). Where the pixel shows the variant is exactly where the extension badges and click-blocks: | Test | On the authoring page (`page_url`) | On any other page | |---|---|---| | single-page (`is_multi_page=false`) | variant applies (raw) | nothing — even if a template exists | | multi-page + morph template | variant applies (raw) | template replays **per element behind the gate**; gate reject ⇒ nothing | | multi-page, no morph template (legacy / non-html) | variant applies (raw) | raw selector apply everywhere (pre-template behavior) | Templates are compiled for **every** html test, but they only *extend* the variant when the customer opts in via the multi-page toggle. The template's `gate` then decides eligibility per selector-matched element — a landing-page card that matches the PDP selector fails the structural gate and is left alone, for **both** arms (symmetric exclusion: a gate-rejected visitor is not bucketed and logs no impression). ## Where templates are compiled Clients (the extension) do not compile templates. The dashboard compiles server-side via `lib/tests/ensure-template.ts` (`ensureMorphTemplate`): if the client payload isn't already morph-format, the `(variant_a, variant_b)` pair is sent to KeaChrome `/v2/templatify`. Wired into every variation-creation path: - `createTestGroup` (test creation, including extension-created tests), - `app/api/tests/[id]/variations` (manual variations), - `app/api/tests/[id]/relaunch-variant`. Plus a **self-heal**: toggling multi-page on (`app/api/tests/[id]/is-multi-page`) compiles a missing template for the active variation before regenerating the script — customers can flip the toggle at any time, so a template must always be obtainable after the fact. All compile paths are best-effort: on failure the variation stores whatever the client sent (or nothing) and the pixel fails closed to single-page behavior. ## Storage and delivery - Stored verbatim from `/v2/templatify` on `test_variations.template` (`{format:"morph", gate, ops}`). Legacy granular op-lists (ops as array, no gate) are treated as absent by everything downstream. - `lib/scriptTemplates.ts` embeds `variant.template = JSON.stringify({gate, doc})` into `KEAK_EMBEDDED_DATA` — **as a JSON string, not an object**. `@keak/shrink`'s `compressEmbeddedData` expand pass recursively renames short keys back to long ones (`v`→`variants`, `st`→`styles`) across the embedded object, which corrupts a morph doc (`"v": 2`, patch keys, anchored-selector keys). A string is opaque to that walk; the pixel `JSON.parse`s it at apply time. A regression test runs a real shrink pass over a stringified template. - `GET /api/tests/active-selectors` additionally returns the active variation's `gate` so the extension can evaluate eligibility without the doc. ## The fail-closed ladder Every stage prefers "do nothing" over a broken page or a polluted experiment: 1. **Compile** — KeaChrome drops ops that risk content leaks; region self-verification drops whole regions that don't reproduce the variant's design; compile failure stores no template. 2. **Delivery** — non-morph / gate-less templates are never embedded; unparseable template strings are discarded by the pixel. 3. **Gate** — candidate element below threshold (or root-tag mismatch) ⇒ no mutation, no bucketing, no impression, both arms excluded alike. 4. **Replay** — unresolved selector ⇒ op skipped; unresolved slotted move ⇒ whole insert skipped; under 60% selectors resolved ⇒ full rollback of the element and the visitor is excluded. ## Extension mirroring The extension shows "Active test" overlays and blocks editing of elements under test. Before gate-awareness it matched purely by selector on whatever page was open — false positives on selector collisions. Now `react-panel/src/lib/templateGate.ts` (verbatim port of the pixel's `evaluateGate`) plus `isElementEligibleForTest` apply the scoping-contract table above to every selector match before badging or blocking. Older dashboards that don't return `gate` degrade gracefully to the previous behavior. ## Conformance and verification Three implementations of the same arithmetic must stay in lockstep: Python (`apply_gate.py` / `templatify.py`), pixel JS (`templateApplier.js`), extension TS (`templateGate.ts`). The KeaChrome benchmark (`benchmarks/templatify/`, harvested noir-blanc pages with selector-collision negatives) generates reference artifacts; the dashboard's `tests/dom/template-applier.spec.ts` asserts JS ≡ Python on gate verdicts and replay output and extension ≡ pixel on gate verdicts (exact equality, scores and reasons included). Change any one of them and run that suite. Benchmark bar (and current state): 0% false-apply on negatives, 100% recall, 100% content preservation, 0 content leaks, design fidelity structurally verified per region (currently 93.8%). ## Operational notes - The dashboard's `CRO_BACKEND_API_URL` points at the deployed KeaChrome — templatify-side fixes are inert in production flows until that service is deployed from the matching branch. - `script.keak.com` serves ~1–2 minutes behind blob regeneration; wait out the edge cache before concluding a script change didn't land. - Headless Chromium throttles `requestAnimationFrame`, which the pixel's apply scheduler uses — probe with a headed browser. `?kv=:B` forces an arm; the `Keak-debug=true` cookie enables pixel logs. - Known recall limitation: a similar page whose markup doesn't resolve the test's element selector at all (e.g. a PDP missing the `main > section` wrapper) is a safe miss — the element-finding layer, not the gate. Candidate fix: `element_signatures` fallback or a save-time validation crawl. --- /docs/platform/testing-loop # The testing loop This is what makes Keak more than a variant generator: a test doesn't just run, it **learns**. A sequential test decides a winner, autopilot generates the next challenger from what won, and the cycle repeats — indefinitely, with no human in the loop. ```mermaid flowchart LR A[Create test
    control vs variant] --> B[Serve impressions
    record conversions] B --> C{SPRT decision} C -->|keep running| B C -->|no difference| B C -->|winner| D[Autovariation:
    generate next variant
    via KeaChrome] D --> E[New round:
    prior winner = control] E --> B ``` ## The data model Three Prisma entities (`prisma/schema.prisma`) carry the loop: - **`Test`** — the experiment: `page_url`, element selector, `type`, `is_active`, the `autovariation` flag, and the SPRT verdict fields (`sprt_decision`, …). - **`TestVariation`** — **one round**: `variant_a` / `variant_b` HTML, their impression + conversion counters, conversion rates, start/end dates, and the `generation_context` describing how that round's variant was made. - **`TestAnalytics`** — individual impression / conversion events (variant, session\_id, timestamp), aggregated on read. A test is created via `POST /api/v1/tests` (or the dashboard), which funnels through `lib/tests/create-test-group.ts` (`createTestGroup`). ## Impressions & conversions The pixel/extension and the API report events to the analytics record endpoint (`app/api/tests/[id]/analytics/record`, `handleUserAnalytics`) and `POST /api/v1/conversions`. Each event is deduplicated per session and written as a `TestAnalytics` row, then rolled up per variant for SPRT. Before any impression is counted, the **billing gate** runs (`entitlements.canReceiveImpressions`): accounts without a card, credit, or under a usage cap have impressions **silently skipped** (the visitor still sees the site). See [Free-tier gate](/docs/billing/free-tier-gate). ## SPRT — how a winner is decided Keak uses a **Sequential Probability Ratio Test** instead of a fixed-horizon test, so it can stop as soon as the evidence is conclusive rather than waiting for a pre-computed sample size. - **Inputs:** impressions and conversions per variant. - **It computes** a log-likelihood ratio comparing "the variants differ by at least the minimum detectable effect" against "no difference," and checks it against two boundaries: `upper = log((1-β)/α)`, `lower = log(β/(1-α))`. - **Decision** (stored in the `SprtDecision` enum): - crosses **upper** → a **winner** (variant A or B), - crosses **lower** → **no meaningful difference** (futility), - otherwise → **keep running** (`continue`; also the cold-start state until both variants have impressions). Significance and power are configurable per test (commonly 95% confidence / 80% power, with a minimum detectable effect). A **minimum-days gate** (`lib/sprt/sprt-min-days.ts`) holds the verdict until the test has run long enough, so an early fluke can't trigger a premature win. Code: `lib/stats/{sprt,sprt-calculator,sprt-decision}.ts` (the math), `lib/sprtService.ts` (applies it to a test). The `process-snapshots` cron recomputes SPRT snapshots for the dashboard; a decision also fires the SPRT-decision email (`lib/sprt/trigger-sprt-decision-email.ts`). ## Autovariation — the autopilot When `autovariation` is on and SPRT reaches a verdict, the loop closes itself: 1. A `VariationGenerationJob` is queued; the **`process-variation-jobs`** cron (every even minute) picks it up. 2. `AutoVariationService.createAutoVariation(testId, winningVariant)` (`lib/autoVariationService.ts`) ends the current round, makes the **prior winner the new control**, and generates a fresh challenger. 3. Generation goes through `lib/ai/v2-variant-service.ts`: `startPageAgentJob` runs KeaChrome's page-agent against the live page (passing the prior winner + its rates as context), and `templatify` compiles the diff into a portable op list for [PDP replay](/docs/keachrome/generation-flows#pdp-templates-templatify). 4. SPRT fields reset to `continue`; email + Slack (`notifySlackVariationCreated`) announce the new round. Each round's `generation_context` feeds the next, so the system accumulates what works. Toggle it with `PATCH /api/v1/tests/:id/autovariation` or [`set_autovariation`](/docs/mcp/tools). ## Media variants A challenger isn't only HTML/copy. Autovariation supports **headline, style, image, and video** variation types, and the image/video media is generated **here in the dashboard** (not KeaChrome — which only *describes* media for context): - `lib/ai/media-generation.ts` + `app/api/media/generate-image` and `app/api/media/generate-video`. - **Image:** OpenAI image models, including image→image variation. - **Video:** Google `veo-3.0-fast-generate-001`, including **image→video** (pass the source image as `imageData`); fal.ai is also wired in. - A standalone **AI image generator** app lives at `app/dashboard/(auth)/apps/ai-image-generator`. Media generation is **not metered separately** — only served impressions are billed.