diff --git a/violet-app-agent/apps/agent/demo/DEMO_SCRIPT.md b/violet-app-agent/apps/agent/demo/DEMO_SCRIPT.md new file mode 100644 index 000000000..b705148f4 --- /dev/null +++ b/violet-app-agent/apps/agent/demo/DEMO_SCRIPT.md @@ -0,0 +1,143 @@ +# Violet App Agent Demo Script + +## The Hook (0-10 seconds) + +**SHOW THE RESULT FIRST** + +``` +[Screen: The finished app - cosmic-observer.localhost:5250/home with 90s nostalgia styling] +``` + +**Voice:** +> "I typed one sentence. 47 seconds later, I had a deployed blog with this styling." + +--- + +## The Problem (10-25 seconds) + +**Voice:** +> "Building apps is slow. Even with Rails, you're still writing migrations, +> controllers, views, CSS... What if you could just describe what you want?" + +``` +[Screen: Split - left side shows traditional Rails workflow (terminal commands), +right side shows a text input box] +``` + +--- + +## The Demo (25-90 seconds) + +### Step 1: Describe Your App + +``` +[Screen: Chat UI with user typing] +``` + +**Voice:** +> "Here's the entire prompt: 'Build me a blog called The Cosmic Observer about astronomy'" + +**Type in the chat:** +``` +Build me a blog called The Cosmic Observer about astronomy. +Make a home page with a hero section. +``` + +### Step 2: Watch It Build + +``` +[Screen: Show the streaming UI with tool calls appearing] +``` + +**Voice:** +> "The agent figures out what to build. Creates a subdomain. +> Generates styled pages. Verifies everything works." + +**Show checkmarks appearing:** +- ✓ create_subdomain → cosmic-observer +- ✓ generate_styled_page → Home with 90s CSS +- ✓ create_page → /home +- ✓ verify_page → Success + +### Step 3: The Result + +``` +[Screen: Navigate to cosmic-observer.localhost:5250/home] +``` + +**Voice:** +> "Cream backgrounds. Georgia serif. Teal and coral accents. +> This isn't generated garbage - it's designed." + +--- + +## The Tech (90-120 seconds) + +**Voice:** +> "Under the hood: LangGraph orchestrating specialized subagents. +> A Template Designer that only uses verified Liquid tags - no hallucinated code. +> Rails runner for direct database access. DHH would approve." + +``` +[Screen: Quick flash of architecture diagram] +``` + +--- + +## The CTA (120-150 seconds) + +``` +[Screen: Hackathon landing page / graphic] +``` + +**Voice:** +> "Want to build something like this? I'm judging the 'Building with AI' track +> at the Violet Rails Hackathon. +> +> Your challenge: take this agent and make it do something we haven't thought of. +> Deploy a marketplace. Generate a SaaS. Build the next big thing. +> +> Link in description. See you there." + +``` +[Screen: + VIOLET RAILS HACKATHON + "Building with AI" Track + + [Register Now] +] +``` + +--- + +## Recording Notes + +### Equipment +- Screen recording: OBS or QuickTime +- Audio: External mic if available +- Resolution: 1920x1080 minimum + +### Pacing +- Speak FAST, pause between sentences (can edit pauses out) +- Total runtime: 2-3 minutes max +- Show, don't tell - let the demo do the work + +### Editing Tips +- Cut all dead air +- Speed up typing (1.5x) +- Add subtle zoom on key moments +- Background music: lo-fi or synthwave (low volume) + +### Platforms +- YouTube: Full version with chapters +- X/Twitter: 60-second cut (hook + demo + CTA) +- LinkedIn: 90-second version with text overlay + +--- + +## B-Roll Ideas + +1. Terminal commands flying by (speed up) +2. The 90s nostalgia CSS file scrolling +3. Tool calls streaming in real-time +4. Before/after comparison (blank page → styled app) diff --git a/violet-app-agent/apps/agent/demo/SOCIAL_POSTS.md b/violet-app-agent/apps/agent/demo/SOCIAL_POSTS.md new file mode 100644 index 000000000..19d3fc6b4 --- /dev/null +++ b/violet-app-agent/apps/agent/demo/SOCIAL_POSTS.md @@ -0,0 +1,210 @@ +# Social Media Posts for Violet App Agent Demo + +## Twitter/X Thread (Main Post) + +### Tweet 1 (The Hook) +``` +I typed one sentence. + +47 seconds later, I had a deployed blog with: +- Cream backgrounds +- Georgia serif typography +- Teal/coral CTAs +- Working navigation + +Here's how 👇 +``` + +### Tweet 2 (The Input) +``` +The entire prompt: + +"Build me a blog called The Cosmic Observer about astronomy. Make a home page with a hero section." + +That's it. No YAML. No configs. No boilerplate. +``` + +### Tweet 3 (The Tech) +``` +Under the hood: + +• LangGraph orchestrating specialized subagents +• Template Designer with verified Liquid tags (no hallucinated code) +• Rails runner for direct DB access +• 90s nostalgia CSS baked in + +DHH would approve. +``` + +### Tweet 4 (The CTA) +``` +Want to build something like this? + +I'm judging the "Building with AI" track at the Violet Rails Hackathon. + +Your challenge: Take this agent and make it do something we haven't thought of. + +Register: [LINK] + +See you there. +``` + +--- + +## LinkedIn Post + +``` +One sentence. One deployed app. + +I've been working on an AI agent that turns natural language descriptions into working Rails applications. + +The result? Type "Build me a blog about astronomy" and get: +✓ A subdomain provisioned +✓ Styled pages with 90s nostalgia CSS +✓ Working navigation and forms +✓ Verified Liquid templates (no hallucinated code) + +The secret sauce: A Template Designer subagent that only uses tags that actually exist in the framework. No runtime errors. No broken pages. + +Tech stack: +• LangGraph for agent orchestration +• Specialized subagents for architecture, CMS, security +• Rails runner for direct database access +• Deterministic template system + +I'm judging the "Building with AI" track at the upcoming Violet Rails Hackathon. + +If you can extend this agent to do something unexpected - deploy a marketplace, generate a SaaS, build the next big thing - I want to see it. + +Link to demo and registration in comments. + +#AI #Rails #DevTools #BuildInPublic +``` + +--- + +## YouTube Short Script (60 seconds) + +``` +[0-5s] HOOK +"One sentence. 47 seconds. A deployed app." + +[5-15s] SHOW THE RESULT +[Screen: Navigate to cosmic-observer.localhost with styled blog] +"Look at this. Cream backgrounds. Georgia serif. Teal and coral buttons. This isn't generated garbage." + +[15-30s] THE INPUT +[Screen: Chat UI with typing] +"Here's the entire prompt: Build me a blog called The Cosmic Observer about astronomy." +[Screen: Tool calls appearing] +"Watch. Subdomain created. Page generated. Verified. Done." + +[30-45s] THE TECH +"The secret? A Template Designer that only uses Liquid tags that actually exist. No hallucinated code. No NoMethodError at runtime." + +[45-60s] CTA +"I'm judging the Building with AI track at the Violet Rails Hackathon. Take this agent. Make it do something we haven't thought of. Link below." +``` + +--- + +## Short Tweet (Single Post) + +``` +Built an AI agent that turns "Build me a blog about astronomy" into a deployed Rails app in 47 seconds. + +Template Designer only uses verified Liquid tags - no hallucinated code. + +Judging "Building with AI" at Violet Rails Hackathon. + +What would you build? + +[DEMO LINK] +``` + +--- + +## Reddit Post (r/rails, r/webdev, r/artificial) + +**Title:** I built an AI agent that deploys Rails apps from natural language - 47 seconds from prompt to styled blog + +**Body:** +``` +Been working on this for a few weeks and wanted to share. + +**The Problem:** +AI agents that generate code tend to hallucinate. They'll write Liquid tags that don't exist, causing runtime errors on pages they create. + +**The Solution:** +A "Template Designer" subagent that: +1. Only uses verified Liquid tags from a whitelist +2. Generates styled HTML with CSS baked in (90s nostalgia aesthetic) +3. Verifies every page renders after creation + +**Demo:** +Type: "Build me a blog called The Cosmic Observer about astronomy" +Get: A working subdomain with styled home page in under a minute + +**Tech:** +- LangGraph for agent orchestration +- Specialized subagents (Architect, CMS Designer, Security, Deployer) +- Rails runner for direct database operations +- Deterministic template system + +**Why I'm posting:** +I'm judging the "Building with AI" track at the Violet Rails Hackathon. Looking for people who want to extend this - deploy marketplaces, generate SaaS apps, whatever. + +Code is open source: [GITHUB LINK] +Demo: [DEMO LINK] + +Would love feedback from the Rails community. +``` + +--- + +## HN Post + +**Title:** Show HN: AI agent that deploys Rails apps from natural language (no hallucinated code) + +``` +I built an AI agent that turns "Build me a blog about astronomy" into a deployed Rails app. + +The key innovation: A "Template Designer" subagent that only uses verified Liquid tags. No hallucinated code = no runtime errors. + +How it works: +1. You describe your app in plain English +2. Agent creates subdomain, generates styled pages, verifies they render +3. You get a working app with 90s nostalgia CSS (cream backgrounds, Georgia serif, teal/coral accents) + +Under 1 minute from prompt to deployed app. + +Tech: LangGraph, specialized subagents, Rails runner for DB access. + +Open source. Looking for feedback and people who want to extend it. + +Demo: [LINK] +GitHub: [LINK] +``` + +--- + +## Recording Checklist + +Before recording the video demo: + +- [ ] Clean browser (no tabs, incognito mode) +- [ ] Fresh subdomain name ready (test first that it doesn't exist) +- [ ] LangGraph server running and healthy +- [ ] Rails server running +- [ ] Screen recording software ready (OBS/QuickTime) +- [ ] External mic connected (if available) +- [ ] Script printed or on second monitor +- [ ] Keyboard shortcuts memorized +- [ ] Energy drink consumed (speak fast!) + +Post-recording: +- [ ] Trim dead air +- [ ] Speed up typing to 1.5x +- [ ] Add zoom on key moments +- [ ] Export at 1080p minimum +- [ ] Thumbnail: split screen (chat input | styled result) diff --git a/violet-app-agent/apps/agent/demo/demo-landing-page.png b/violet-app-agent/apps/agent/demo/demo-landing-page.png new file mode 100644 index 000000000..0a9446b07 Binary files /dev/null and b/violet-app-agent/apps/agent/demo/demo-landing-page.png differ diff --git a/violet-app-agent/apps/agent/demo/index.html b/violet-app-agent/apps/agent/demo/index.html new file mode 100644 index 000000000..8c90bd137 --- /dev/null +++ b/violet-app-agent/apps/agent/demo/index.html @@ -0,0 +1,712 @@ + + + + + + Violet App Agent | Build Apps with Natural Language + + + + + + + + + + +
+
+ + +
+
+ +
+ +
+
+

+ One sentence.
+ One deployed app. +

+

+ Describe your app in plain English. Get a styled, working Rails app in under a minute. +

+ +
+
+ + +
+
+
+
+ + + + Violet App Agent +
+ +
+
+
You:
+
+ Build me a blog called
+ "The Cosmic Observer"
+ about astronomy.
+ Make a home page with
+ a hero section. +
+
+ +
+

Agent Workflow

+
+ + create_subdomain + cosmic-observer + 2.3s +
+
+ + generate_styled_page + Home + 90s CSS + 1.8s +
+
+ + create_page + /home + 0.9s +
+
+ + verify_page + 200 OK + 0.4s +
+
+
+ + +
+

Result: cosmic-observer.localhost

+
+
+ https://cosmic-observer.yourdomain.com/home +
+
+

The Cosmic Observer

+

Exploring the wonders of the universe

+ +
+
+
+
+
+
+ + +
+
+

Built for developers who value their time

+
+
+
💬
+

Natural Language

+

Describe what you want in plain English. No YAML configs. No boilerplate.

+
+
+
🎨
+

90s Nostalgia CSS

+

Cream backgrounds. Georgia serif. Teal and coral accents. Designed, not generated.

+
+
+
+

Verified Templates

+

Only uses Liquid tags that actually exist. No hallucinated code. No runtime errors.

+
+
+
🤖
+

LangGraph Agents

+

Specialized subagents for architecture, CMS design, security, and deployment.

+
+
+
🛤
+

Rails Runner

+

Direct database access via Rails runner. DHH-approved patterns.

+
+
+
🚀
+

Deploy Anywhere

+

Local, GitHub, or Heroku. One command deployment with GitHub Actions.

+
+
+
+
+ + +
+
+
Coming Soon
+

Violet Rails Hackathon

+

+ Take this agent and make it do something we haven't thought of. + Deploy a marketplace. Generate a SaaS. Build the next big thing. +

+ +
+ +
+

Best UI/UX

+

Most polished user experience

+
+
+

Most Creative

+

Unexpected use cases

+
+
+ +

+ I'll be judging the "Building with AI" track. Show me what you've got. +

+ + Register for Hackathon +
+
+
+ + + + diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-01-landing-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-01-landing-page.png new file mode 100644 index 000000000..c39f7c37b Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-01-landing-page.png differ diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-02-home-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-02-home-page.png new file mode 100644 index 000000000..ca6b1b98f Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-02-home-page.png differ diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-03-stories-empty.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-03-stories-empty.png new file mode 100644 index 000000000..f9be8b120 Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-03-stories-empty.png differ diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-04-toronto-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-04-toronto-page.png new file mode 100644 index 000000000..24ffe4be2 Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-04-toronto-page.png differ diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-05-jersey-city-page.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-05-jersey-city-page.png new file mode 100644 index 000000000..b46a12c2b Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-05-jersey-city-page.png differ diff --git a/violet-app-agent/apps/agent/e2e_screenshots/eval-06-write-error.png b/violet-app-agent/apps/agent/e2e_screenshots/eval-06-write-error.png new file mode 100644 index 000000000..b705b74a0 Binary files /dev/null and b/violet-app-agent/apps/agent/e2e_screenshots/eval-06-write-error.png differ diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/agent.py b/violet-app-agent/apps/agent/src/violet_app_agent/agent.py index c697deab6..31e30ef9f 100644 --- a/violet-app-agent/apps/agent/src/violet_app_agent/agent.py +++ b/violet-app-agent/apps/agent/src/violet_app_agent/agent.py @@ -20,6 +20,12 @@ content_researcher_subagent, deployer_subagent, security_subagent, + template_designer_subagent, + list_templates, + select_template, + get_liquid_tag, + generate_styled_page, + verify_page, ) from violet_app_agent.tools import ( create_namespace, @@ -62,6 +68,12 @@ def create_violet_app_agent( create_namespace, # CMS and pages create_page, + # Template Designer tools (deterministic, verified Liquid tags) + list_templates, + select_template, + get_liquid_tag, + generate_styled_page, + verify_page, # Deployment trigger_deployment, ], @@ -71,6 +83,7 @@ def create_violet_app_agent( content_researcher_subagent, deployer_subagent, security_subagent, + template_designer_subagent, # Deterministic template generation ], system_prompt=SYSTEM_PROMPT, checkpointer=True if (use_memory and not running_in_langgraph_api) else None, diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py b/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py index 3b5a4ea3d..4c369d0cf 100644 --- a/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py +++ b/violet-app-agent/apps/agent/src/violet_app_agent/prompts.py @@ -96,7 +96,27 @@ 1. Create subdomain 2. Create each API namespace 3. Generate forms -4. Create CMS pages +4. Create styled CMS pages with 90s nostalgia design + +**CRITICAL: For ALL pages, use the Template Designer workflow:** +``` +1. generate_styled_page(page_type, slot_values, nav_links) + → Returns styled HTML with 90s nostalgia CSS + +2. create_page(subdomain, title, slug, page_type="custom", content=styled_html) + → Creates the page in CMS + +3. verify_page(subdomain, slug) + → Confirms the page renders without errors +``` + +**90s Nostalgia Style** (default for all pages): +- Cream backgrounds (#fdf6e3) +- Georgia serif typography +- Teal/coral accent colors +- Paper-white content cards +- Generous whitespace +- CSS Grid layouts Report progress as you go. @@ -168,6 +188,46 @@ - Deploy to production via GitHub ``` +## Template Designer Page Types + +Use `generate_styled_page` with these page types: + +| Page Type | Use For | Key Slots | +|-----------|---------|-----------| +| home | Landing page with hero | site_title, headline, tagline, main_content | +| category | Topic/category landing | headline, tagline, main_content | +| post_index | List all posts | headline, tagline | +| post_show | Single post view | headline, main_content | +| write | Form submission page | headline, tagline (uses render_form) | +| about | About/info page | headline, main_content | + +**Example: Creating a styled home page** +```python +# 1. Generate styled HTML +result = generate_styled_page( + page_type="home", + slot_values={ + "site_title": "The Woodchuck Inquirer", + "headline": "How Much Wood?", + "tagline": "Exploring life's great questions", + "main_content": "

Welcome to our blog...

" + } +) +styled_html = result["html"] + +# 2. Create the page +create_page( + subdomain="woodchuck-studies", + title="Home", + slug="home", + page_type="custom", + content=styled_html +) + +# 3. Verify it works +verify_page(subdomain="woodchuck-studies", path="/home") +``` + ## Property Types | Type | Use For | Example | diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py index 41b55ca59..e1b61566c 100644 --- a/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py +++ b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/__init__.py @@ -5,6 +5,17 @@ from violet_app_agent.subagents.content_researcher import content_researcher_subagent from violet_app_agent.subagents.deployer import deployer_subagent from violet_app_agent.subagents.security import security_subagent +from violet_app_agent.subagents.template_designer import ( + template_designer_subagent, + list_templates, + select_template, + get_liquid_tag, + generate_styled_page, + verify_page, + VERIFIED_LIQUID_TAGS, + FORBIDDEN_TAGS, + BLOG_TEMPLATES, +) __all__ = [ "architect_subagent", @@ -12,4 +23,14 @@ "content_researcher_subagent", "deployer_subagent", "security_subagent", + # Template Designer subagent and tools + "template_designer_subagent", + "list_templates", + "select_template", + "get_liquid_tag", + "generate_styled_page", + "verify_page", + "VERIFIED_LIQUID_TAGS", + "FORBIDDEN_TAGS", + "BLOG_TEMPLATES", ] diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/subagents/template_designer.py b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/template_designer.py new file mode 100644 index 000000000..c7f6ad0ee --- /dev/null +++ b/violet-app-agent/apps/agent/src/violet_app_agent/subagents/template_designer.py @@ -0,0 +1,548 @@ +""" +Template Designer Subagent + +Following Deep Agent architecture: DIAGNOSE -> MULTI-EXPERT -> ARTIFACT-FIRST -> DOMAIN-NATIVE + +This subagent is responsible for: +1. SELECT appropriate templates from the verified template library +2. CUSTOMIZE templates with user-specific content +3. APPLY styling (90s nostalgia CSS by default) +4. VERIFY each page renders successfully after creation + +CRITICAL: Only uses verified Liquid tags that exist in Violet Rails. +""" + +import os +import requests +from pathlib import Path +from typing import Any + +from langchain_core.tools import tool +from langgraph.prebuilt import create_react_agent +from langchain_anthropic import ChatAnthropic + + +# Template directory path +TEMPLATES_DIR = Path(__file__).parent.parent / "templates" + + +# Verified Liquid tags that exist in Violet Rails +VERIFIED_LIQUID_TAGS = { + # Content tags (SAFE - always work) + "text": "{{ cms:text {identifier} }}", + "markdown": "{{ cms:markdown {identifier} }}", + "wysiwyg": "{{ cms:wysiwyg {identifier} }}", + + # Asset tags (SAFE) + "asset": "{{ cms:asset {identifier}, as: image }}", + "file": "{{ cms:file {identifier} }}", + + # Snippet tags (SAFE - if snippet exists) + "snippet": "{{ cms:snippet {identifier} }}", + + # Collection tags (SAFE) + "collection": "{{ cms:collection {identifier} | namespace: '{namespace}' }}", + + # Form tags (USE render_form NOT render_api_form) + "form": "{{ render_form | namespace: '{namespace}' | submit_text: '{submit_text}' }}", +} + +# Tags that DO NOT exist - agent must never use these +FORBIDDEN_TAGS = { + "render_api_form": "Does not exist - use render_form instead", + "api_resource": "Does not exist", + "render_api": "Does not exist", +} + + +# Blog templates library +BLOG_TEMPLATES = { + "home": { + "name": "Blog Home Page", + "file": "blog/home.liquid", + "slots": { + "site_title": "Site title displayed in browser tab", + "headline": "Main headline (h1)", + "tagline": "Subtitle under headline", + "footer_text": "Footer copyright/attribution", + }, + "description": "Homepage with hero section, category cards, and featured posts", + }, + "category": { + "name": "Category Page", + "file": "blog/category.liquid", + "slots": { + "category_title": "Category name (h1)", + "category_description": "Category description", + "category_content": "Main content area", + }, + "description": "Category landing page with description and posts", + }, + "post_index": { + "name": "Posts Index", + "file": "blog/post_index.liquid", + "slots": { + "page_title": "Page title", + "intro_text": "Introduction paragraph", + }, + "description": "List all posts with filtering options", + }, + "post_show": { + "name": "Single Post", + "file": "blog/post_show.liquid", + "slots": { + "post_title": "Post title", + "post_content": "Full post content", + "post_author": "Author name", + "post_date": "Publication date", + }, + "description": "Individual post view with full content", + }, + "write": { + "name": "Submit Story Form", + "file": "blog/write.liquid", + "slots": { + "page_title": "Page title", + "intro_text": "Introduction/instructions", + }, + "form_namespace": "stories", + "form_submit_text": "Share Your Story", + "description": "Story submission form - uses render_form (NOT render_api_form)", + }, + "about": { + "name": "About Page", + "file": "blog/about.liquid", + "slots": { + "page_title": "Page title", + "about_content": "About section content", + }, + "description": "About page with author/site information", + }, +} + + +TEMPLATE_SUBAGENT_PROMPT = """You are the Template Designer subagent for the Violet App Agent. + +Your role is to: +1. SELECT appropriate templates from the verified template library +2. CUSTOMIZE templates with user-specific content +3. APPLY styling (90s nostalgia CSS by default) +4. VERIFY each page renders successfully after creation + +## CRITICAL RULES + +1. **ONLY use Liquid tags from VERIFIED_LIQUID_TAGS** + - text, markdown, wysiwyg: Content display + - asset, file: Media + - snippet: Reusable content blocks + - collection: Data collections + - form: User forms (render_form) + +2. **NEVER use these tags (they don't exist):** + - render_api_form (DOES NOT EXIST - use render_form) + - api_resource (DOES NOT EXIST) + - render_api (DOES NOT EXIST) + +3. **ALWAYS verify pages after creation** + - Use verify_page tool after every create_page + - Report errors immediately + - Do not mark complete until verified + +4. **Use 90s nostalgia CSS by default** + - Cream backgrounds, paper-white cards + - Georgia serif typography + - Teal/coral accents + - CSS Grid layout + - Generous whitespace + +## Available Templates + +- home: Homepage with hero, categories, featured posts +- category: Category landing page +- post_index: All posts listing +- post_show: Single post view +- write: Story submission form (uses render_form, NOT render_api_form) +- about: About page + +## Example Workflow + +1. User wants a home page -> select_template("home") +2. Fill slots with content +3. Apply 90s nostalgia CSS +4. Create page via apply_template +5. Verify page renders -> verify_page +6. Report success or errors +""" + + +def load_css() -> str: + """Load the 90s nostalgia CSS file.""" + css_path = TEMPLATES_DIR / "styles" / "90s_nostalgia.css" + if css_path.exists(): + return css_path.read_text() + return "" + + +def validate_liquid_content(content: str) -> tuple[bool, list[str]]: + """ + Validate Liquid content only uses known tags. + + Returns: + (is_valid, list of errors) + """ + errors = [] + + for forbidden, reason in FORBIDDEN_TAGS.items(): + if forbidden in content: + errors.append(f"Forbidden tag '{forbidden}': {reason}") + + return len(errors) == 0, errors + + +@tool +def list_templates() -> dict[str, Any]: + """ + List all available blog templates with their descriptions. + + Returns: + Dictionary of template names to their metadata + """ + return { + name: { + "name": t["name"], + "description": t["description"], + "slots": list(t["slots"].keys()), + } + for name, t in BLOG_TEMPLATES.items() + } + + +@tool +def select_template(page_type: str) -> dict[str, Any]: + """ + Select the appropriate template for a page type. + + Args: + page_type: One of 'home', 'category', 'post_index', 'post_show', 'write', 'about' + + Returns: + Template definition with content slots and requirements + """ + if page_type not in BLOG_TEMPLATES: + return { + "error": f"Unknown page type: {page_type}", + "available": list(BLOG_TEMPLATES.keys()), + } + + template = BLOG_TEMPLATES[page_type] + result = { + "page_type": page_type, + "template_name": template["name"], + "template_file": template["file"], + "slots": template["slots"], + "description": template["description"], + "style": "90s_nostalgia.css", + } + + # Add form info if applicable + if "form_namespace" in template: + result["form"] = { + "namespace": template["form_namespace"], + "submit_text": template["form_submit_text"], + "liquid_tag": f"{{{{ render_form | namespace: '{template['form_namespace']}' | submit_text: '{template['form_submit_text']}' }}}}", + "warning": "NEVER use render_api_form - it does not exist!", + } + + return result + + +@tool +def get_liquid_tag(tag_type: str, identifier: str = "", namespace: str = "", submit_text: str = "Submit") -> dict[str, Any]: + """ + Get the correct Liquid tag syntax for a given tag type. + + Args: + tag_type: Type of tag (text, markdown, collection, form, etc.) + identifier: Content identifier + namespace: Namespace for collection/form tags + submit_text: Button text for form tags + + Returns: + The correct Liquid syntax to use + """ + if tag_type not in VERIFIED_LIQUID_TAGS: + return { + "error": f"Unknown tag type: {tag_type}", + "available": list(VERIFIED_LIQUID_TAGS.keys()), + } + + template = VERIFIED_LIQUID_TAGS[tag_type] + result = template.format( + identifier=identifier, + namespace=namespace, + submit_text=submit_text, + ) + + return { + "tag_type": tag_type, + "syntax": result, + "warning": "Never use render_api_form - it does not exist!" if tag_type == "form" else None, + } + + +@tool +def verify_page(subdomain: str, path: str) -> dict[str, Any]: + """ + Verify a page was created successfully and renders without errors. + + CRITICAL: Call this after EVERY page creation to ensure it works. + + Args: + subdomain: The subdomain name (e.g., 'conscious-observer') + path: The page path (e.g., '/home') + + Returns: + Verification results including success status and any errors + """ + # Construct URL - handle path with or without leading slash + if not path.startswith("/"): + path = f"/{path}" + + url = f"http://{subdomain}.localhost:5250{path}" + + try: + response = requests.get(url, timeout=10) + + # Check for known error patterns + has_error = any(err in response.text for err in [ + "NoMethodError", + "NameError", + "SyntaxError", + "undefined method", + "ActionView::Template::Error", + "Action Controller: Exception", + ]) + + # Check for specific forbidden tag errors + forbidden_tag_errors = [] + for tag in FORBIDDEN_TAGS: + if tag in response.text: + forbidden_tag_errors.append(f"Page uses forbidden tag: {tag}") + + return { + "success": response.status_code == 200 and not has_error, + "url": url, + "status_code": response.status_code, + "has_content": len(response.text) > 100, + "has_error": has_error, + "forbidden_tag_errors": forbidden_tag_errors, + "content_length": len(response.text), + "recommendation": "Page OK" if (response.status_code == 200 and not has_error) else "Fix errors before proceeding", + } + + except requests.exceptions.ConnectionError: + return { + "success": False, + "url": url, + "error": "Connection refused - is the Rails server running?", + "recommendation": "Start Rails server: bin/rails server -p 5250", + } + except requests.exceptions.Timeout: + return { + "success": False, + "url": url, + "error": "Request timed out", + "recommendation": "Check server status and try again", + } + except Exception as e: + return { + "success": False, + "url": url, + "error": str(e), + "recommendation": "Investigate the error", + } + + +@tool +def generate_styled_page( + page_type: str, + slot_values: dict[str, str], + nav_links: list[dict[str, str]] | None = None, +) -> dict[str, Any]: + """ + Generate a complete styled page HTML with 90s nostalgia CSS. + + Args: + page_type: Template type (home, category, write, etc.) + slot_values: Dict mapping slot names to content values + nav_links: Optional list of nav links [{"text": "Home", "href": "/"}] + + Returns: + Complete HTML ready to be used as page content + """ + if page_type not in BLOG_TEMPLATES: + return { + "error": f"Unknown page type: {page_type}", + "available": list(BLOG_TEMPLATES.keys()), + } + + template = BLOG_TEMPLATES[page_type] + css = load_css() + + # Default navigation + if nav_links is None: + nav_links = [ + {"text": "Home", "href": "/home"}, + {"text": "Stories", "href": "/stories"}, + {"text": "Write", "href": "/write"}, + ] + + nav_html = "\n".join( + f' {link["text"]}' + for link in nav_links + ) + + # Get slot values with defaults + site_title = slot_values.get("site_title", "My Blog") + headline = slot_values.get("headline", "Welcome") + tagline = slot_values.get("tagline", "") + main_content = slot_values.get("main_content", "") + footer_text = slot_values.get("footer_text", "Made with Violet Rails") + + # Generate page-type specific content + if page_type == "home": + content_section = f""" +
+

{headline}

+

{tagline}

+
+ Read Stories + Share Your Story +
+
+ +
+

Explore

+
+ {main_content} +
+
+""" + elif page_type == "write": + # CRITICAL: Use render_form, NOT render_api_form + form_namespace = template.get("form_namespace", "stories") + form_submit = template.get("form_submit_text", "Submit") + content_section = f""" +
+

{headline}

+

{tagline}

+ + {{{{ render_form | namespace: '{form_namespace}' | submit_text: '{form_submit}' }}}} +
+""" + elif page_type == "category": + content_section = f""" +
+

{headline}

+

{tagline}

+ +
+ {main_content} +
+
+""" + else: + content_section = f""" +
+

{headline}

+ {f'

{tagline}

' if tagline else ''} +
+ {main_content} +
+
+""" + + # Validate no forbidden tags + is_valid, errors = validate_liquid_content(content_section) + if not is_valid: + return { + "error": "Generated content contains forbidden Liquid tags", + "forbidden_tags_found": errors, + "fix": "Remove forbidden tags and use verified alternatives", + } + + # Build complete HTML + html = f""" + + + + + {site_title} + + + +
+
+ +
+ +
+{content_section} +
+ + +
+ + +""" + + return { + "html": html, + "page_type": page_type, + "slots_filled": list(slot_values.keys()), + "style": "90s_nostalgia", + "validated": True, + "next_step": "Create page using create_page tool, then verify with verify_page", + } + + +from deepagents import SubAgent + +template_designer_subagent: SubAgent = { + "name": "template-designer-subagent", + "description": """Use this subagent for blog/CMS page creation with verified templates: +- Selecting appropriate page templates (home, category, write, about) +- Getting correct Liquid tag syntax (NEVER hallucinate tags) +- Generating styled HTML with 90s nostalgia CSS +- Verifying pages render correctly after creation + +CRITICAL: Only uses VERIFIED Liquid tags. Never uses render_api_form (doesn't exist).""", + "system_prompt": TEMPLATE_SUBAGENT_PROMPT, + "tools": [ + list_templates, + select_template, + get_liquid_tag, + generate_styled_page, + verify_page, + ], +} + + +# Export for use in main agent +__all__ = [ + "template_designer_subagent", + "list_templates", + "select_template", + "get_liquid_tag", + "generate_styled_page", + "verify_page", + "VERIFIED_LIQUID_TAGS", + "FORBIDDEN_TAGS", + "BLOG_TEMPLATES", +] diff --git a/violet-app-agent/apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css b/violet-app-agent/apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css new file mode 100644 index 000000000..8ca204fce --- /dev/null +++ b/violet-app-agent/apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css @@ -0,0 +1,478 @@ +/* + * 90s Low-Tech Nostalgia CSS + * + * Design Philosophy: "Don't Make Me Think Revisited" + * - Clear, obvious navigation + * - Every click should be obvious + * - Room to breathe + * - Satisfice: Users scan, not read + */ + +:root { + /* Colors: Muted, warm, nostalgic */ + --bg-cream: #f5f0e6; + --bg-paper: #fffef9; + --text-ink: #2c2c2c; + --text-muted: #5a5a5a; + --accent-teal: #2a9d8f; + --accent-coral: #e76f51; + --border-soft: #d4cfc4; + + /* Typography: Georgia for that web 1.0 feel */ + --font-serif: Georgia, "Times New Roman", serif; + --font-mono: "Courier New", Courier, monospace; + + /* Sizes: Generous, readable */ + --text-sm: 0.875rem; + --text-base: 1.125rem; + --text-lg: 1.5rem; + --text-xl: 2rem; + --text-2xl: 2.5rem; + + /* Spacing: Room to breathe */ + --space-xs: 0.5rem; + --space-sm: 1rem; + --space-md: 2rem; + --space-lg: 4rem; + --space-xl: 8rem; +} + +/* Reset */ +*, *::before, *::after { + box-sizing: border-box; +} + +/* Base: Paper-like background */ +body { + font-family: var(--font-serif); + font-size: var(--text-base); + line-height: 1.7; + color: var(--text-ink); + background: var(--bg-cream); + margin: 0; + padding: 0; + min-height: 100vh; +} + +/* CSS Grid Layout: Simple content-width container */ +.container { + display: grid; + grid-template-columns: + [full-start] minmax(var(--space-md), 1fr) + [content-start] minmax(0, 720px) + [content-end] minmax(var(--space-md), 1fr) + [full-end]; + row-gap: var(--space-lg); + padding-top: var(--space-md); + padding-bottom: var(--space-xl); +} + +.container > * { + grid-column: content; +} + +/* Full-bleed elements */ +.full-bleed { + grid-column: full; +} + +/* Typography: Semantic, scannable */ +h1, h2, h3 { + font-family: var(--font-serif); + font-weight: normal; + letter-spacing: -0.02em; + margin: 0 0 var(--space-sm); +} + +h1 { + font-size: var(--text-2xl); + line-height: 1.2; +} + +h2 { + font-size: var(--text-xl); + color: var(--text-muted); +} + +h3 { + font-size: var(--text-lg); +} + +p { + margin: 0 0 var(--space-sm); +} + +/* Links: Underlined, obvious */ +a { + color: var(--accent-teal); + text-decoration: underline; + text-underline-offset: 3px; + transition: color 0.2s ease; +} + +a:hover { + color: var(--accent-coral); +} + +/* Cards: Subtle elevation */ +.card { + background: var(--bg-paper); + border: 1px solid var(--border-soft); + padding: var(--space-md); + margin-bottom: var(--space-md); +} + +.card h3 { + margin-top: 0; +} + +/* Blockquotes: Pull quotes */ +blockquote { + font-style: italic; + border-left: 3px solid var(--accent-teal); + padding-left: var(--space-md); + margin: var(--space-md) 0; + color: var(--text-muted); +} + +blockquote cite { + display: block; + margin-top: var(--space-sm); + font-style: normal; + font-size: var(--text-sm); +} + +/* Navigation: Simple, horizontal */ +nav { + display: flex; + gap: var(--space-md); + padding: var(--space-md) 0; + border-bottom: 1px solid var(--border-soft); +} + +nav a { + text-decoration: none; + color: var(--text-ink); +} + +nav a:hover { + text-decoration: underline; +} + +nav a.active { + color: var(--accent-teal); +} + +/* Header */ +header { + margin-bottom: var(--space-md); +} + +/* Hero section */ +.hero { + text-align: center; + padding: var(--space-lg) 0; +} + +.hero h1 { + margin-bottom: var(--space-sm); +} + +.hero p { + color: var(--text-muted); + font-size: var(--text-lg); +} + +/* CTA buttons in hero */ +.hero-cta { + display: flex; + gap: var(--space-sm); + justify-content: center; + margin-top: var(--space-md); +} + +.hero-cta a { + padding: var(--space-sm) var(--space-md); + text-decoration: none; + border: 1px solid var(--border-soft); + background: var(--bg-paper); +} + +.hero-cta a:hover { + background: var(--accent-teal); + color: white; + border-color: var(--accent-teal); +} + +/* Forms: Clean, accessible */ +.form-group { + margin-bottom: var(--space-md); +} + +label { + display: block; + margin-bottom: var(--space-xs); + font-weight: bold; +} + +input, textarea, select { + width: 100%; + padding: var(--space-sm); + font-family: var(--font-serif); + font-size: var(--text-base); + border: 1px solid var(--border-soft); + background: var(--bg-paper); + border-radius: 0; +} + +input:focus, textarea:focus, select:focus { + outline: 2px solid var(--accent-teal); + outline-offset: 2px; +} + +textarea { + min-height: 200px; + resize: vertical; +} + +button, .btn { + font-family: var(--font-serif); + font-size: var(--text-base); + padding: var(--space-sm) var(--space-md); + background: var(--accent-teal); + color: white; + border: none; + cursor: pointer; + text-decoration: none; + display: inline-block; +} + +button:hover, .btn:hover { + background: var(--accent-coral); +} + +/* Post cards grid */ +.posts-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--space-md); +} + +.post-card { + background: var(--bg-paper); + border: 1px solid var(--border-soft); + padding: var(--space-md); +} + +.post-card h3 { + margin-top: 0; + margin-bottom: var(--space-xs); +} + +.post-card h3 a { + text-decoration: none; + color: var(--text-ink); +} + +.post-card h3 a:hover { + color: var(--accent-teal); +} + +.post-card .meta { + font-size: var(--text-sm); + color: var(--text-muted); + margin-bottom: var(--space-sm); +} + +.post-card .excerpt { + color: var(--text-muted); +} + +/* Category pills */ +.category-pill { + display: inline-block; + font-size: var(--text-sm); + padding: var(--space-xs) var(--space-sm); + background: var(--bg-cream); + border: 1px solid var(--border-soft); + color: var(--text-muted); + text-decoration: none; + margin-right: var(--space-xs); + margin-bottom: var(--space-xs); +} + +.category-pill:hover { + background: var(--accent-teal); + color: white; + border-color: var(--accent-teal); +} + +/* Section spacing */ +section { + margin-bottom: var(--space-lg); +} + +section h2 { + margin-bottom: var(--space-md); +} + +/* Content cards (categories on home) */ +.content-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--space-md); +} + +.content-card { + background: var(--bg-paper); + border: 1px solid var(--border-soft); + padding: var(--space-md); + text-align: center; +} + +.content-card h3 { + margin-top: 0; +} + +.content-card p { + color: var(--text-muted); + margin-bottom: var(--space-md); +} + +/* Footer */ +footer { + margin-top: var(--space-xl); + padding: var(--space-md) 0; + border-top: 1px solid var(--border-soft); + font-size: var(--text-sm); + color: var(--text-muted); + text-align: center; +} + +/* Article/Post styling */ +article { + max-width: 100%; +} + +article header { + margin-bottom: var(--space-lg); + text-align: center; +} + +article header h1 { + margin-bottom: var(--space-sm); +} + +article header .meta { + color: var(--text-muted); + font-size: var(--text-sm); +} + +article .content { + line-height: 1.8; +} + +article .content p { + margin-bottom: var(--space-md); +} + +article .content img { + max-width: 100%; + height: auto; + margin: var(--space-md) 0; +} + +/* About section styling */ +.about { + background: var(--bg-paper); + border: 1px solid var(--border-soft); + padding: var(--space-md); + margin: var(--space-md) 0; +} + +.about h3 { + margin-top: 0; +} + +/* Timeline/Journey styling */ +.timeline { + border-left: 2px solid var(--border-soft); + padding-left: var(--space-md); + margin: var(--space-md) 0; +} + +.timeline-item { + margin-bottom: var(--space-md); + position: relative; +} + +.timeline-item::before { + content: ""; + position: absolute; + left: calc(-1 * var(--space-md) - 5px); + top: 8px; + width: 10px; + height: 10px; + background: var(--accent-teal); + border-radius: 50%; +} + +.timeline-item h3 { + margin-top: 0; + margin-bottom: var(--space-xs); +} + +/* Empty state */ +.empty-state { + text-align: center; + padding: var(--space-xl) var(--space-md); + color: var(--text-muted); +} + +.empty-state p { + margin-bottom: var(--space-md); +} + +/* Responsive: Mobile-first */ +@media (max-width: 600px) { + :root { + --text-base: 1rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --text-2xl: 2rem; + --space-md: 1.5rem; + --space-lg: 2rem; + --space-xl: 4rem; + } + + nav { + flex-direction: column; + gap: var(--space-sm); + } + + .hero-cta { + flex-direction: column; + } + + .posts-grid, + .content-cards { + grid-template-columns: 1fr; + } +} + +/* Print styles */ +@media print { + body { + background: white; + color: black; + } + + nav, footer, .hero-cta { + display: none; + } + + a { + color: black; + text-decoration: underline; + } +} diff --git a/violet-app-agent/docs/RFC-002-template-ecosystem.md b/violet-app-agent/docs/RFC-002-template-ecosystem.md new file mode 100644 index 000000000..1aedc629b --- /dev/null +++ b/violet-app-agent/docs/RFC-002-template-ecosystem.md @@ -0,0 +1,892 @@ +# RFC-002: Template Ecosystem & Agent Actions Critique + +**Status:** Draft +**Author:** Violet App Agent Team +**Created:** 2025-12-09 +**Related PR:** #1719 +**Depends On:** RFC-001 (Plan Mode UX) + +## Executive Summary + +This RFC critiques the agent's actions during the "Conscious Observer" blog build, defines success criteria for verification, and proposes a Template Ecosystem that ensures deterministic, working page generation with 90's low-tech nostalgia styling. + +--- + +## Part 1: Composite Evaluation of Agent Journey + +### What the Agent Built + +| Page | URL | Status | Quality | +|------|-----|--------|---------| +| Landing | `/` | Default | Generic Violet Rails welcome | +| Home | `/home` | Working | Good structure, 3 content cards | +| Toronto | `/toronto` | Working | Rich narrative, blockquote | +| Jersey City | `/jersey-city` | Working | Most personalized, references chaiwithjai.com | +| Stories | `/stories` | Empty | Blank page - no content | +| Write | `/write` | **BROKEN** | `NoMethodError: undefined method 'render_api_form'` | + +### Screenshot Evidence + +``` +eval-01-landing-page.png → Generic "Hello from conscious-observer" +eval-02-home-page.png → 3 cards: Toronto, Jersey City, Observation +eval-03-stories-empty.png → Completely blank white page +eval-04-toronto-page.png → Rich content with cultural narrative +eval-05-jersey-city-page.png → Jai's story, chai references, November 2025 +eval-06-write-error.png → NoMethodError stack trace +``` + +### What Went Right + +1. **Subdomain Creation**: `conscious-observer` created successfully +2. **Content Generation**: Agent generated compelling, personalized content: + - Toronto: "cultural crossroads", "international origins" + - Jersey City: "November 2025", "Heights", references to chaiwithjai.com +3. **Information Architecture**: Logical page hierarchy with clear navigation +4. **Personal Voice**: Content matches the "Jai Bhagat" persona well + +### What Went Wrong + +1. **Non-Existent Liquid Tag**: Agent used `render_api_form` which doesn't exist in Violet Rails + - Should have used `render_form` or built a static HTML form + - This is a **domain knowledge gap** - agent doesn't know available Liquid tags + +2. **Empty Stories Page**: `/stories` was created but has no content + - Either the page content failed to save, or agent didn't populate it + +3. **No Verification**: Agent didn't verify pages actually render before reporting success + - A simple GET request to each page would have caught the `/write` error + +4. **No Styling**: Pages are unstyled browser defaults + - No CSS was applied + - Functional but not visually appealing + +--- + +## Part 2: Success Criteria for Verification + +### Definition of "Done" for Page Creation + +A page is only successfully created when ALL criteria pass: + +```python +class PageVerificationCriteria: + """Success criteria for page creation verification.""" + + MUST_PASS = { + "page_exists": "Page record exists in database", + "page_renders": "GET request returns 200 (not 500)", + "no_errors": "No NoMethodError, NameError, or SyntaxError", + "content_present": "Page has visible content (not blank)", + "links_work": "Internal links resolve to existing pages", + } + + SHOULD_PASS = { + "styling_applied": "CSS styles are present", + "semantic_html": "Uses proper heading hierarchy", + "mobile_friendly": "Viewport meta tag present", + } + + NICE_TO_HAVE = { + "accessibility": "Alt text on images, proper ARIA", + "performance": "Page loads in < 3 seconds", + } +``` + +### Verification Tool Implementation + +```python +@tool +def verify_page(subdomain: str, path: str) -> dict: + """ + Verify a page was created successfully and renders without errors. + + Args: + subdomain: The subdomain name (e.g., 'conscious-observer') + path: The page path (e.g., '/home') + + Returns: + dict with verification results + """ + url = f"http://{subdomain}.localhost:5250{path}" + + try: + response = requests.get(url, timeout=10) + + return { + "success": response.status_code == 200, + "status_code": response.status_code, + "has_content": len(response.text) > 100, + "has_error": "NoMethodError" in response.text or "Error" in response.text, + "content_length": len(response.text), + "url": url, + } + except Exception as e: + return { + "success": False, + "error": str(e), + "url": url, + } +``` + +### E2E Verification Test Suite + +```python +class TestBlogDelivery: + """E2E tests verifying the delivered blog artifact.""" + + @pytest.fixture + def subdomain(self): + return "conscious-observer" + + def test_home_page_renders(self, subdomain): + """Home page should render with content cards.""" + result = verify_page(subdomain, "/home") + assert result["success"], f"Home page failed: {result}" + assert result["has_content"], "Home page is empty" + assert not result["has_error"], "Home page has errors" + + def test_all_linked_pages_work(self, subdomain): + """All links from home should resolve.""" + links = ["/stories", "/write", "/toronto", "/jersey-city"] + failures = [] + + for link in links: + result = verify_page(subdomain, link) + if not result["success"] or result["has_error"]: + failures.append(f"{link}: {result}") + + assert not failures, f"Broken links: {failures}" + + def test_no_500_errors(self, subdomain): + """No page should return a 500 error.""" + pages = ["/", "/home", "/stories", "/write", "/toronto", "/jersey-city"] + + for page in pages: + result = verify_page(subdomain, page) + assert result.get("status_code") != 500, f"{page} returns 500" +``` + +--- + +## Part 3: Real-World Expectations + +### What Users Expect vs What They Get + +| Expectation | Current Reality | Gap | +|-------------|-----------------|-----| +| "A working blog" | 4/6 pages work | `/stories` empty, `/write` broken | +| "Professional styling" | Browser defaults | No CSS applied | +| "Ready to add content" | Missing forms | Can't submit stories | +| "Mobile friendly" | Unstyled = responsive-ish | No viewport meta | + +### User Journey Pain Points + +1. **First Impression**: Landing at `/` shows generic welcome, not custom blog +2. **Broken Flow**: Clicking "Share Your Story" leads to error page +3. **Empty Sections**: Stories page is blank - looks like something broke +4. **No Visual Identity**: Plain HTML doesn't convey blog personality + +### Real-World Success Definition + +```yaml +definition_of_done: + functional: + - All 6 pages render without errors + - Navigation links all work + - Story submission form functional + - Content displays correctly + + visual: + - Consistent styling across pages + - Typography reflects blog personality + - Color scheme applied + - Mobile responsive + + content: + - Home has introductory content + - Category pages have descriptions + - At least one sample post + - Clear calls-to-action +``` + +--- + +## Part 4: Deterministic Tool Calls + +### The Problem + +Agent generated Liquid code using non-existent helpers: + +```liquid + +{{ render_api_form | namespace: "story" | submit_text: "Share Your Story" }} + + +{{ cms:snippet story_form }} +
...
+``` + +### Root Cause + +The agent: +1. Knows Violet Rails has forms +2. Doesn't know the exact Liquid tag syntax +3. "Hallucinated" a plausible-sounding tag name +4. Didn't verify the tag exists before using it + +### Solution: Deterministic Template Library + +Instead of generating Liquid code, agent selects from verified templates: + +```python +VERIFIED_LIQUID_TAGS = { + # Content tags (SAFE - always work) + "text": "{{ cms:text identifier }}", + "markdown": "{{ cms:markdown identifier }}", + "wysiwyg": "{{ cms:wysiwyg identifier }}", + + # Asset tags (SAFE) + "asset": "{{ cms:asset identifier, as: image }}", + "file": "{{ cms:file identifier }}", + + # Snippet tags (SAFE - if snippet exists) + "snippet": "{{ cms:snippet identifier }}", + + # Collection tags (SAFE) + "collection": "{{ cms:collection identifier | namespace: 'name' }}", + + # Form tags (USE render_form NOT render_api_form) + "form": "{{ render_form | namespace: 'name' }}", +} + +FORBIDDEN_TAGS = { + "render_api_form": "Does not exist - use render_form", + "api_resource": "Does not exist", +} +``` + +### Validation Before Execution + +```python +def validate_liquid_content(content: str) -> tuple[bool, list[str]]: + """ + Validate Liquid content only uses known tags. + + Returns: + (is_valid, list of errors) + """ + errors = [] + + for forbidden, reason in FORBIDDEN_TAGS.items(): + if forbidden in content: + errors.append(f"Forbidden tag '{forbidden}': {reason}") + + return len(errors) == 0, errors +``` + +--- + +## Part 5: Template Ecosystem for Blog Use Case + +### Design Philosophy: "Don't Make Me Think Revisited" + +Following Steve Krug's principles: + +1. **Don't make users think** - Clear, obvious navigation +2. **Every click should be obvious** - No mystery meat navigation +3. **Get rid of half the words, then get rid of half of what's left** +4. **Satisfice** - Users don't read, they scan + +### Template Structure + +``` +templates/ +├── blog/ +│ ├── base.liquid # Base layout with CSS Grid +│ ├── home.liquid # Featured posts + categories +│ ├── post_index.liquid # All posts with filters +│ ├── post_show.liquid # Single post view +│ ├── category.liquid # Posts by category +│ ├── author.liquid # Author profile + posts +│ └── write.liquid # Story submission form +├── components/ +│ ├── header.liquid # Navigation +│ ├── footer.liquid # Footer with links +│ ├── post_card.liquid # Post preview card +│ └── category_pill.liquid # Category badge +└── styles/ + └── 90s-nostalgia.css # The aesthetic +``` + +### 90's Low-Tech Nostalgia CSS + +```css +/* 90s-nostalgia.css - Design that breathes */ + +:root { + /* Colors: Muted, warm, nostalgic */ + --bg-cream: #f5f0e6; + --bg-paper: #fffef9; + --text-ink: #2c2c2c; + --text-muted: #5a5a5a; + --accent-teal: #2a9d8f; + --accent-coral: #e76f51; + --border-soft: #d4cfc4; + + /* Typography: Georgia for that web 1.0 feel */ + --font-serif: Georgia, "Times New Roman", serif; + --font-mono: "Courier New", Courier, monospace; + + /* Sizes: Generous, readable */ + --text-sm: 0.875rem; + --text-base: 1.125rem; + --text-lg: 1.5rem; + --text-xl: 2rem; + --text-2xl: 2.5rem; + + /* Spacing: Room to breathe */ + --space-xs: 0.5rem; + --space-sm: 1rem; + --space-md: 2rem; + --space-lg: 4rem; + --space-xl: 8rem; +} + +/* Base: Paper-like background */ +body { + font-family: var(--font-serif); + font-size: var(--text-base); + line-height: 1.7; + color: var(--text-ink); + background: var(--bg-cream); + margin: 0; + padding: 0; +} + +/* CSS Grid Layout: Simple 12-column */ +.container { + display: grid; + grid-template-columns: + [full-start] minmax(var(--space-md), 1fr) + [content-start] minmax(0, 720px) + [content-end] minmax(var(--space-md), 1fr) + [full-end]; + row-gap: var(--space-lg); +} + +.container > * { + grid-column: content; +} + +/* Full-bleed elements */ +.full-bleed { + grid-column: full; +} + +/* Typography: Semantic, scannable */ +h1, h2, h3 { + font-family: var(--font-serif); + font-weight: normal; + letter-spacing: -0.02em; + margin: 0 0 var(--space-sm); +} + +h1 { + font-size: var(--text-2xl); + line-height: 1.2; +} + +h2 { + font-size: var(--text-xl); + color: var(--text-muted); +} + +h3 { + font-size: var(--text-lg); +} + +p { + margin: 0 0 var(--space-sm); +} + +/* Links: Underlined, obvious */ +a { + color: var(--accent-teal); + text-decoration: underline; + text-underline-offset: 3px; +} + +a:hover { + color: var(--accent-coral); +} + +/* Cards: Subtle elevation */ +.card { + background: var(--bg-paper); + border: 1px solid var(--border-soft); + padding: var(--space-md); + margin-bottom: var(--space-md); +} + +/* Blockquotes: Pull quotes */ +blockquote { + font-style: italic; + border-left: 3px solid var(--accent-teal); + padding-left: var(--space-md); + margin: var(--space-md) 0; + color: var(--text-muted); +} + +/* Navigation: Simple, horizontal */ +nav { + display: flex; + gap: var(--space-md); + padding: var(--space-md) 0; + border-bottom: 1px solid var(--border-soft); +} + +nav a { + text-decoration: none; + color: var(--text-ink); +} + +nav a:hover { + text-decoration: underline; +} + +/* Forms: Clean, accessible */ +.form-group { + margin-bottom: var(--space-md); +} + +label { + display: block; + margin-bottom: var(--space-xs); + font-weight: bold; +} + +input, textarea { + width: 100%; + padding: var(--space-sm); + font-family: var(--font-serif); + font-size: var(--text-base); + border: 1px solid var(--border-soft); + background: var(--bg-paper); +} + +textarea { + min-height: 200px; + resize: vertical; +} + +button { + font-family: var(--font-serif); + font-size: var(--text-base); + padding: var(--space-sm) var(--space-md); + background: var(--accent-teal); + color: white; + border: none; + cursor: pointer; +} + +button:hover { + background: var(--accent-coral); +} + +/* Post cards grid */ +.posts-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--space-md); +} + +/* Category pills */ +.category-pill { + display: inline-block; + font-size: var(--text-sm); + padding: var(--space-xs) var(--space-sm); + background: var(--bg-cream); + border: 1px solid var(--border-soft); + color: var(--text-muted); + text-decoration: none; +} + +/* Footer */ +footer { + margin-top: var(--space-xl); + padding: var(--space-md) 0; + border-top: 1px solid var(--border-soft); + font-size: var(--text-sm); + color: var(--text-muted); +} + +/* Responsive: Mobile-first */ +@media (max-width: 600px) { + :root { + --text-base: 1rem; + --text-xl: 1.5rem; + --text-2xl: 2rem; + --space-md: 1.5rem; + --space-lg: 2rem; + } + + nav { + flex-direction: column; + gap: var(--space-sm); + } +} +``` + +### Template: Home Page + +```liquid + + + + + + + {{ cms:text site_title }} + + + +
+
+ +
+ +
+
+

{{ cms:text headline }}

+

{{ cms:text tagline }}

+
+ +
+

Explore

+
+ {{ cms:snippet category_cards }} +
+
+ + +
+ + +
+ + +``` + +### Template: Write/Submit Page (FIXED) + +```liquid + + + + + + + + Share Your Story | {{ cms:text site_title }} + + + +
+
+ +
+ +
+

Share Your Story

+

{{ cms:text write_intro }}

+ + + {{ render_form | namespace: 'stories' | submit_text: 'Share Your Story' }} +
+ + +
+ + +``` + +--- + +## Part 6: Template Subagent Architecture + +### Deep Agent Integration + +Following the DIAGNOSE → MULTI-EXPERT → ARTIFACT-FIRST → DOMAIN-NATIVE pattern: + +``` + ┌─────────────────────┐ + │ Main Agent │ + │ (Orchestrator) │ + └─────────┬───────────┘ + │ + ┌───────────────────────┼───────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ + │ Architect │ │ Template │ │ Deployer │ + │ Subagent │ │ Subagent │◄──NEW │ Subagent │ + └──────────────┘ └──────────────┘ └──────────────┘ + │ │ │ + │ │ │ + ┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ + │ Data Model │ │ Template │ │ GitHub │ + │ Design │ │ Selection │ │ Deployment │ + └─────────────┘ │ + Styling │ └─────────────┘ + │ + Verify │ + └─────────────┘ +``` + +### Template Subagent Definition + +```python +# src/violet_app_agent/subagents/template_designer.py + +from langchain_core.tools import tool +from langgraph.prebuilt import create_react_agent +from langchain_anthropic import ChatAnthropic + +TEMPLATE_SUBAGENT_PROMPT = """You are the Template Designer subagent for the Violet App Agent. + +Your role is to: +1. SELECT appropriate templates from the verified template library +2. CUSTOMIZE templates with user-specific content +3. APPLY styling (90s nostalgia CSS by default) +4. VERIFY each page renders successfully after creation + +CRITICAL RULES: +- ONLY use Liquid tags from VERIFIED_LIQUID_TAGS +- NEVER use render_api_form (it doesn't exist) - use render_form +- ALWAYS verify pages after creation +- Report any rendering errors immediately + +Available Templates: +- blog/home.liquid - Homepage with hero, categories, featured posts +- blog/post_index.liquid - All posts listing +- blog/post_show.liquid - Single post view +- blog/category.liquid - Posts filtered by category +- blog/write.liquid - Story submission form (uses render_form) + +Default Style: 90s-nostalgia.css +- Cream background, paper-white cards +- Georgia serif typography +- Teal/coral accents +- CSS Grid layout +- Generous whitespace +""" + +@tool +def select_template(page_type: str) -> dict: + """ + Select the appropriate template for a page type. + + Args: + page_type: One of 'home', 'post_index', 'post_show', 'category', 'write' + + Returns: + Template definition with content slots + """ + templates = { + "home": { + "file": "blog/home.liquid", + "slots": ["headline", "tagline", "category_cards", "featured_posts"], + "style": "90s-nostalgia.css", + }, + "write": { + "file": "blog/write.liquid", + "slots": ["write_intro"], + "form": "render_form", # NOT render_api_form! + "style": "90s-nostalgia.css", + }, + # ... other templates + } + return templates.get(page_type, {}) + + +@tool +def apply_template( + subdomain: str, + path: str, + template_name: str, + content: dict +) -> dict: + """ + Apply a template to create a page with styling. + + Args: + subdomain: Target subdomain + path: Page path (e.g., '/home') + template_name: Template to use (e.g., 'blog/home.liquid') + content: Dict of slot names to content values + + Returns: + Result with page URL and verification status + """ + # Load template + # Fill slots + # Apply CSS + # Create page via rails_runner + # VERIFY the page renders + verification = verify_page(subdomain, path) + + return { + "page_url": f"http://{subdomain}.localhost:5250{path}", + "template": template_name, + "verified": verification["success"], + "errors": verification.get("errors", []), + } + + +@tool +def verify_page(subdomain: str, path: str) -> dict: + """Verify a page renders without errors.""" + # Implementation from Part 2 + pass + + +def create_template_subagent(): + """Create the Template Designer subagent.""" + model = ChatAnthropic(model="claude-sonnet-4-20250514", temperature=0) + + tools = [ + select_template, + apply_template, + verify_page, + ] + + return create_react_agent( + model, + tools, + state_modifier=TEMPLATE_SUBAGENT_PROMPT, + ) +``` + +### Integration with Main Agent + +```python +# In agent.py - Updated tool list + +def create_agent(): + """Create the main Violet App Agent with template subagent.""" + + # Subagents + architect = create_architect_subagent() + template_designer = create_template_subagent() # NEW + deployer = create_deployer_subagent() + + tools = [ + # Infrastructure + create_subdomain, + create_namespace, + + # Templates (NEW - deterministic) + select_template, + apply_template, + verify_page, + + # Deployment + trigger_deployment, + ] + + return create_react_agent(model, tools, state_modifier=SYSTEM_PROMPT) +``` + +--- + +## Part 7: Implementation Roadmap + +### Phase 1: Fix Critical Issues (Immediate) + +1. [ ] Fix `/write` page - replace `render_api_form` with `render_form` +2. [ ] Add content to `/stories` page +3. [ ] Implement `verify_page` tool +4. [ ] Add verification step after each `create_page` call + +### Phase 2: Template Ecosystem (Week 1) + +1. [ ] Create verified template library (6 templates) +2. [ ] Implement `select_template` tool +3. [ ] Implement `apply_template` tool with CSS injection +4. [ ] Write 90s-nostalgia.css + +### Phase 3: Template Subagent (Week 2) + +1. [ ] Create Template Designer subagent +2. [ ] Integrate with main agent graph +3. [ ] Add template selection to Plan Phase +4. [ ] E2E tests for template application + +### Phase 4: Verification Suite (Week 3) + +1. [ ] Full verification test suite +2. [ ] Automated screenshot comparison +3. [ ] Mobile responsiveness checks +4. [ ] Performance baseline tests + +--- + +## Appendix: Verified Liquid Tags Reference + +```yaml +# SAFE TO USE - These tags exist and work +content_tags: + - "{{ cms:text identifier }}" + - "{{ cms:markdown identifier }}" + - "{{ cms:wysiwyg identifier }}" + +asset_tags: + - "{{ cms:asset identifier, as: image }}" + - "{{ cms:file identifier }}" + +snippet_tags: + - "{{ cms:snippet identifier }}" + +collection_tags: + - "{{ cms:collection identifier | namespace: 'name' }}" + +form_tags: + - "{{ render_form | namespace: 'name' }}" # CORRECT + +# FORBIDDEN - These DO NOT exist +forbidden: + - render_api_form # Use render_form instead + - api_resource # Does not exist + - render_api # Does not exist +``` + +--- + +## Conclusion + +The Conscious Observer blog build revealed critical gaps in the agent's domain knowledge and verification processes. By implementing: + +1. **Deterministic templates** with verified Liquid tags +2. **Mandatory verification** after page creation +3. **A Template Subagent** following Deep Agent architecture +4. **90s nostalgia styling** for visual identity + +We can ensure that future builds produce working, styled, verified artifacts that match user expectations from Day 0. + +The template ecosystem transforms the agent from a "generate and hope" model to a "select, apply, and verify" model - dramatically improving reliability while maintaining flexibility for customization. diff --git a/violet-app-agent/docs/TEMPLATE_ECOSYSTEM_CONTRIBUTING.md b/violet-app-agent/docs/TEMPLATE_ECOSYSTEM_CONTRIBUTING.md new file mode 100644 index 000000000..53504f993 --- /dev/null +++ b/violet-app-agent/docs/TEMPLATE_ECOSYSTEM_CONTRIBUTING.md @@ -0,0 +1,588 @@ +# Template Ecosystem Contributing Guide + +> Build AI agents that deploy Rails apps from natural language. + +This guide helps hackathon participants understand, extend, and contribute to the Template Ecosystem feature in the Violet App Agent. + +## Quick Links + +| Resource | Description | +|----------|-------------| +| [Demo Landing Page](../apps/agent/demo/index.html) | Try the agent live | +| [RFC-002](./RFC-002-template-ecosystem.md) | Design philosophy and architecture | +| [Template Designer](../apps/agent/src/violet_app_agent/subagents/template_designer.py) | Core implementation | +| [90s Nostalgia CSS](../apps/agent/src/violet_app_agent/templates/styles/90s_nostalgia.css) | Design system | + +--- + +## What is the Template Ecosystem? + +The Template Ecosystem solves a critical problem: **AI agents hallucinate non-existent code**. + +When we asked LLMs to generate Liquid templates for Violet Rails, they invented tags like `render_api_form` that don't exist, causing runtime errors. + +**Our solution:** A deterministic template system with: +1. **Verified Liquid Tags** - Whitelist of tags that actually exist +2. **90s Nostalgia CSS** - Automatic styling baked into every page +3. **Page Verification** - HTTP checks confirm pages render without errors + +### The Workflow + +``` +1. generate_styled_page(page_type, slot_values, nav_links) + → Returns styled HTML with 90s nostalgia CSS + +2. create_page(subdomain, title, slug, content=styled_html) + → Creates the page in CMS + +3. verify_page(subdomain, slug) + → Confirms the page renders without errors +``` + +--- + +## Architecture Overview + +``` +violet-app-agent/ +├── apps/agent/ +│ ├── src/violet_app_agent/ +│ │ ├── agent.py # Main agent +│ │ ├── prompts.py # System prompts +│ │ ├── subagents/ +│ │ │ ├── __init__.py # Exports subagents +│ │ │ └── template_designer.py # Template Designer (this feature!) +│ │ └── templates/ +│ │ └── styles/ +│ │ └── 90s_nostalgia.css # Design system +│ └── demo/ +│ ├── index.html # Landing page +│ └── chat_ui_streaming.html # Chat interface +└── docs/ + ├── RFC-002-template-ecosystem.md # Design doc + └── TEMPLATE_ECOSYSTEM_CONTRIBUTING.md # This file +``` + +### Deep Agent Pattern + +The agent follows the **DIAGNOSE → MULTI-EXPERT → ARTIFACT-FIRST → DOMAIN-NATIVE** pattern: + +```python +Main Agent (Orchestrator) + ├── Architect Subagent (Data model design) + ├── Template Designer Subagent ← THIS FEATURE + ├── CMS Designer Subagent (Content management) + ├── Content Researcher Subagent (Content generation) + ├── Deployer Subagent (GitHub deployment) + └── Security Subagent (Security checks) +``` + +--- + +## Setup for Development + +### Prerequisites + +- Python 3.11+ +- Poetry +- Rails server running on port 5250 +- PostgreSQL database + +### Quick Start + +```bash +# Clone and setup +cd violet_rails/violet-app-agent/apps/agent + +# Install dependencies +poetry install + +# Copy environment file +cp .env.example .env +# Edit .env with your ANTHROPIC_API_KEY + +# Start the agent +poetry run langgraph dev --port 8123 + +# In another terminal, start Rails +cd violet_rails +bin/rails server -p 5250 +``` + +### Verify Setup + +```bash +# Check agent is running +curl http://localhost:8123/health + +# Check Rails is running +curl http://localhost:5250 +``` + +--- + +## Key Code Locations + +### 1. Template Designer Subagent + +**File:** `apps/agent/src/violet_app_agent/subagents/template_designer.py` + +```python +# Verified Liquid tags that exist in Violet Rails +VERIFIED_LIQUID_TAGS = { + "text": "{{ cms:text {identifier} }}", + "markdown": "{{ cms:markdown {identifier} }}", + "wysiwyg": "{{ cms:wysiwyg {identifier} }}", + "asset": "{{ cms:asset {identifier}, as: image }}", + "file": "{{ cms:file {identifier} }}", + "snippet": "{{ cms:snippet {identifier} }}", + "collection": "{{ cms:collection {identifier} | namespace: '{namespace}' }}", + "form": "{{ render_form | namespace: '{namespace}' | submit_text: '{submit_text}' }}", +} + +# Tags that DO NOT exist - agent must never use these +FORBIDDEN_TAGS = { + "render_api_form": "Does not exist - use render_form instead", + "api_resource": "Does not exist", + "render_api": "Does not exist", +} +``` + +### 2. Template Library + +```python +BLOG_TEMPLATES = { + "home": { + "name": "Blog Home Page", + "slots": { + "site_title": "Site title displayed in browser tab", + "headline": "Main headline (h1)", + "tagline": "Subtitle under headline", + "footer_text": "Footer copyright/attribution", + }, + "description": "Homepage with hero section and featured posts", + }, + "category": { ... }, + "post_index": { ... }, + "post_show": { ... }, + "write": { ... }, # Has form_namespace for render_form + "about": { ... }, +} +``` + +### 3. Five Core Tools + +| Tool | Purpose | +|------|---------| +| `list_templates()` | Show available page templates | +| `select_template(page_type)` | Get template definition with slots | +| `get_liquid_tag(tag_type, identifier)` | Return verified Liquid syntax | +| `generate_styled_page(page_type, slot_values)` | Create HTML with CSS | +| `verify_page(subdomain, path)` | HTTP check for render errors | + +--- + +## Extension Ideas for Hackathon + +### Beginner (2-4 hours) + +1. **New Page Template** - Add a "contact" or "faq" template +2. **CSS Theme Variant** - Create a brutalist or minimalist theme +3. **New Liquid Tag** - Add a verified tag from Violet Rails + +### Intermediate (4-8 hours) + +4. **E-commerce Templates** - Product catalog, pricing pages +5. **SEO Optimizer Subagent** - Meta tags, OG tags, heading analysis +6. **Accessibility Checker** - WCAG compliance verification + +### Advanced (8+ hours) + +7. **Multi-page Generator** - Generate entire sites from descriptions +8. **Template Composition** - Header + body + footer system +9. **A/B Testing Support** - Template variations with analytics + +--- + +## How to Add a New Template + +### Step 1: Define the Template + +```python +# In template_designer.py, add to BLOG_TEMPLATES: + +"contact": { + "name": "Contact Page", + "file": "blog/contact.liquid", + "slots": { + "page_title": "Page title", + "intro_text": "Introduction text", + "email": "Contact email address", + "phone": "Phone number (optional)", + }, + "form_namespace": "contact", # If it has a form + "form_submit_text": "Send Message", + "description": "Contact page with form and info", +}, +``` + +### Step 2: Add Generation Logic + +```python +# In generate_styled_page(), add a new elif block: + +elif page_type == "contact": + form_namespace = template.get("form_namespace", "contact") + form_submit = template.get("form_submit_text", "Send") + + content_section = f""" +
+

{slot_values.get("page_title", "Contact Us")}

+

{slot_values.get("intro_text", "")}

+ +
+

Email: {slot_values.get("email", "")}

+

Phone: {slot_values.get("phone", "")}

+
+ + {{{{ render_form | namespace: '{form_namespace}' | submit_text: '{form_submit}' }}}} +
+""" +``` + +### Step 3: Test It + +```python +# Test in Python REPL +from violet_app_agent.subagents.template_designer import generate_styled_page + +result = generate_styled_page( + page_type="contact", + slot_values={ + "page_title": "Get In Touch", + "intro_text": "We'd love to hear from you!", + "email": "hello@example.com", + } +) + +print(result["html"][:500]) # Preview first 500 chars +``` + +--- + +## How to Add a New CSS Theme + +### Step 1: Create the CSS File + +```css +/* In templates/styles/brutalist.css */ + +:root { + --bg-cream: #ffffff; + --bg-paper: #ffffff; + --text-ink: #000000; + --text-muted: #333333; + --accent-teal: #000000; + --accent-coral: #ff0000; + --border-soft: #000000; + + --font-serif: "Courier New", monospace; + --font-mono: "Courier New", monospace; +} + +/* Override specific components */ +.card { + border: 3px solid black; + box-shadow: 5px 5px 0 black; +} + +a { + text-decoration: none; + border-bottom: 2px solid black; +} +``` + +### Step 2: Add Theme Selection + +```python +# In template_designer.py + +CSS_THEMES = { + "90s_nostalgia": "90s_nostalgia.css", + "brutalist": "brutalist.css", + "minimalist": "minimalist.css", +} + +def load_css(theme: str = "90s_nostalgia") -> str: + css_file = CSS_THEMES.get(theme, "90s_nostalgia.css") + css_path = TEMPLATES_DIR / "styles" / css_file + return css_path.read_text() if css_path.exists() else "" +``` + +### Step 3: Update generate_styled_page + +```python +def generate_styled_page( + page_type: str, + slot_values: dict[str, str], + nav_links: list[dict[str, str]] | None = None, + theme: str = "90s_nostalgia", # Add theme parameter +) -> dict[str, Any]: + css = load_css(theme) + # ... rest of function +``` + +--- + +## How to Add a New Subagent + +### Step 1: Create the Subagent File + +```python +# In subagents/seo_optimizer.py + +""" +SEO Optimizer Subagent + +Analyzes and improves page SEO. +""" + +from langchain_core.tools import tool +from deepagents import SubAgent + +@tool +def analyze_seo(html: str) -> dict: + """Analyze HTML for SEO issues.""" + issues = [] + + if "" not in html: + issues.append("Missing <title> tag") + if 'meta name="description"' not in html: + issues.append("Missing meta description") + if "<h1>" not in html: + issues.append("Missing H1 tag") + + return { + "score": 100 - (len(issues) * 20), + "issues": issues, + "recommendations": [f"Fix: {issue}" for issue in issues], + } + +@tool +def generate_meta_tags(title: str, description: str, keywords: list[str]) -> str: + """Generate SEO meta tags.""" + return f''' +<title>{title} + + + + +''' + +SEO_SUBAGENT_PROMPT = """You are the SEO Optimizer subagent. + +Your role is to: +1. ANALYZE pages for SEO issues +2. GENERATE meta tags +3. RECOMMEND improvements + +Always prioritize: +- Clear, descriptive titles +- Compelling meta descriptions +- Proper heading hierarchy +""" + +seo_optimizer_subagent: SubAgent = { + "name": "seo-optimizer-subagent", + "description": "Analyzes and improves page SEO with meta tags and recommendations.", + "system_prompt": SEO_SUBAGENT_PROMPT, + "tools": [analyze_seo, generate_meta_tags], +} +``` + +### Step 2: Export from __init__.py + +```python +# In subagents/__init__.py + +from .seo_optimizer import ( + seo_optimizer_subagent, + analyze_seo, + generate_meta_tags, +) + +__all__ = [ + # ... existing exports + "seo_optimizer_subagent", + "analyze_seo", + "generate_meta_tags", +] +``` + +### Step 3: Register in Main Agent + +```python +# In agent.py + +from violet_app_agent.subagents import ( + # ... existing imports + seo_optimizer_subagent, + analyze_seo, + generate_meta_tags, +) + +def create_violet_app_agent(): + return create_deep_agent( + tools=[ + # ... existing tools + analyze_seo, + generate_meta_tags, + ], + subagents=[ + # ... existing subagents + seo_optimizer_subagent, + ], + ) +``` + +--- + +## Testing Your Changes + +### Unit Tests + +```python +# In tests/test_template_designer.py + +def test_contact_template(): + result = generate_styled_page( + page_type="contact", + slot_values={"page_title": "Contact"} + ) + + assert "error" not in result + assert "Contact" in result["html"] + assert "render_form" in result["html"] + assert "render_api_form" not in result["html"] # Forbidden! + +def test_brutalist_theme(): + result = generate_styled_page( + page_type="home", + slot_values={"headline": "Test"}, + theme="brutalist" + ) + + assert "Courier New" in result["html"] +``` + +### Integration Tests + +```python +def test_full_page_creation(): + # 1. Generate + html_result = generate_styled_page( + page_type="home", + slot_values={"headline": "Test Site"} + ) + + # 2. Create (mock or use test subdomain) + # create_page(subdomain="test", title="Home", content=html_result["html"]) + + # 3. Verify + # verify_result = verify_page(subdomain="test", path="/home") + # assert verify_result["success"] is True +``` + +### Run Tests + +```bash +cd violet-app-agent/apps/agent +poetry run pytest -v +poetry run pytest -v tests/test_template_designer.py +``` + +--- + +## Submission Checklist + +Before submitting your hackathon project: + +- [ ] Code runs without errors +- [ ] All new templates use only verified Liquid tags +- [ ] Pages verify successfully with `verify_page()` +- [ ] Tests pass +- [ ] README explains what you built +- [ ] Demo video (2-3 minutes) shows it working + +--- + +## Common Pitfalls + +### 1. Hallucinated Liquid Tags + +**Wrong:** +```python +"{{ render_api_form | namespace: 'contact' }}" # DOESN'T EXIST! +``` + +**Right:** +```python +"{{ render_form | namespace: 'contact' | submit_text: 'Send' }}" +``` + +### 2. Forgetting to Verify + +Always call `verify_page()` after creating a page: + +```python +# After create_page() +result = verify_page(subdomain="my-site", path="/contact") +if not result["success"]: + print(f"Page has errors: {result}") +``` + +### 3. Missing Slots + +Templates have required slots. Check them: + +```python +template = select_template("home") +print(template["slots"]) # Shows required slot names +``` + +--- + +## Getting Help + +- **GitHub Issues:** [restarone/violet_rails](https://github.com/restarone/violet_rails/issues) +- **Demo:** Open `demo/index.html` and click "Try It Live" +- **RFC:** Read `docs/RFC-002-template-ecosystem.md` for design rationale + +--- + +## Design Philosophy + +From RFC-002: + +> **"Don't Make Me Think Revisited"** (Steve Krug) +> - Clear, obvious navigation +> - Every click should be obvious +> - Room to breathe +> - Users scan, not read + +The 90s nostalgia aesthetic: +- Cream backgrounds (#f5f0e6) +- Georgia serif typography +- Teal (#2a9d8f) and coral (#e76f51) accents +- CSS Grid layout with generous whitespace +- Paper-white cards with subtle borders + +--- + +## License + +This project is part of Violet Rails, licensed under [MIT License](../../LICENSE). + +--- + +**Built for the Violet Rails Hackathon - "Building with AI" Track**