# Widget Generator

> Build a spec-to-file widget system where users describe a widget in natural language and Claude scaffolds a working React component into the project. Use when the user wants to add an "AI-generate this UI panel" feature, an extensible plug-in system using filesystem discovery, or wants users of their app to author components without touching code.

- Skill: `traderspost/widget-generator` (Agent Skill)
- Install (CLI): `npx skillmds@latest add traderspost/widget-generator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/traderspost/widget-generator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: traderspost (https://skillmd.com/u/traderspost)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/traderspost/widget-generator

---


# widget-generator

The command-dash dashboard lets users describe a widget ("show me a heatmap of S&P sectors") and have Claude scaffold a working React component into the running app. The trick: a widget is a **folder** that Vite auto-discovers, so writing files to the right location is all it takes — no rebuild, no registration step.

## When to use

Trigger this skill when the user wants to:
- Add an AI-powered "extend this app" feature where end-users describe new components
- Build a plug-in system that's discovered by directory scan + filesystem glob
- Generate UI code from natural language and persist it for later editing
- Hot-reload newly written modules without restarting the dev server

## Architecture

```
apps/web/src/widgets/
├── gex/
│   ├── manifest.json    ← name, description, defaultSize
│   └── index.tsx        ← default-export React component
├── polymarket/
│   ├── manifest.json
│   └── index.tsx
└── <newly-generated-slug>/   ← Claude writes here
    ├── manifest.json
    └── index.tsx
```

The registry (`src/lib/widget-registry.ts`):

```ts
import type { ComponentType } from 'react'

const modules = import.meta.glob('../widgets/*/index.tsx', { eager: true })
const manifests = import.meta.glob('../widgets/*/manifest.json', { eager: true })

export function loadWidgets() {
  return Object.entries(modules).map(([path, mod]) => {
    const slug = path.split('/')[2]
    const manifest = (manifests[`../widgets/${slug}/manifest.json`] as any).default
    return { slug, manifest, Component: (mod as any).default as ComponentType }
  })
}
```

Vite resolves `import.meta.glob` with `{ eager: true }` at module-load time. When a new folder appears under `src/widgets/`, Vite's HMR picks it up on the next save and triggers a page reload — no rebuild needed.

## The Claude prompt

Two priorities: a strict output schema, and a concrete example. The model fills in the rest.

```python
WIDGET_PROMPT = """You are scaffolding a new widget for the command-dash trading dashboard.

The dashboard auto-discovers widgets under `apps/web/src/widgets/<slug>/`. Each
widget folder must contain exactly two files:

1. `manifest.json` with shape:
```json
{
  "name": "Human Readable Name",
  "description": "One-sentence purpose.",
  "defaultSize": { "w": 6, "h": 8 }
}
```
Grid units: w is 1–12, h is 1–20.

2. `index.tsx` exporting a default React component. The component must:
- Be self-contained (no external state stores)
- Fit any size — use flex/grid to fill its container
- Use Tailwind classes and these palette tokens:
  - `bg-bg-card`, `bg-bg-elevated`, `border-border`
  - `text-text-primary`, `text-text-secondary`, `text-text-dim`
  - `text-accent`, `text-bull`, `text-bear`
- Fetch data via `fetch('/api/...')` if needed

Example widget for reference:

```tsx
import { useEffect, useState } from 'react'

export default function GexWidget() {
  const [data, setData] = useState<any>(null)
  useEffect(() => {
    fetch('/api/gex/snapshot').then(r => r.json()).then(setData)
  }, [])
  if (!data) return <div className="p-4 text-text-dim text-sm">Loading…</div>
  return (
    <div className="h-full flex flex-col bg-bg-card border border-border rounded-lg p-3">
      <div className="text-xs text-text-secondary mb-2">SPX Gamma</div>
      <div className="text-2xl text-text-primary">${(data.gammaNotional / 1e9).toFixed(2)}B</div>
    </div>
  )
}
```

User's request:
{prompt}

Respond with EXACTLY this JSON shape — no prose, no markdown fences:
{{
  "slug": "kebab-case-slug",
  "manifest": {{ "name": "...", "description": "...", "defaultSize": {{"w": 6, "h": 8}} }},
  "tsx": "<the full contents of index.tsx as a string>"
}}
"""
```

## Endpoint that writes the files

```python
import json, re
from pathlib import Path
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from anthropic import AsyncAnthropic

WIDGETS_DIR = Path("apps/web/src/widgets")
SLUG_RE = re.compile(r"^[a-z][a-z0-9-]{1,40}$")
router = APIRouter()


class Req(BaseModel):
    prompt: str


@router.post("/api/widgets/generate")
async def generate(req: Req):
    client = AsyncAnthropic()
    msg = await client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        messages=[{"role": "user", "content": WIDGET_PROMPT.format(prompt=req.prompt)}],
    )
    text = "".join(b.text for b in msg.content if hasattr(b, "text"))

    # Tolerate optional code fences from the model
    cleaned = text.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.split("\n", 1)[1].rsplit("```", 1)[0]

    spec = json.loads(cleaned)
    slug = spec["slug"]
    if not SLUG_RE.match(slug):
        raise HTTPException(400, f"invalid slug: {slug}")

    target = WIDGETS_DIR / slug
    if target.exists():
        raise HTTPException(409, f"{slug} already exists")
    target.mkdir(parents=True)
    (target / "manifest.json").write_text(json.dumps(spec["manifest"], indent=2))
    (target / "index.tsx").write_text(spec["tsx"])

    return {"slug": slug, "manifest": spec["manifest"]}
```

## Trust model and security caveats

This pattern executes Claude-authored React code in the user's browser. That's fine when:
- The dashboard runs **locally** on a single user's machine
- The Anthropic API key is theirs
- They can read the generated code before clicking refresh

It is **not** safe for multi-tenant deployments. Don't ship this pattern to production without:
- Sandboxing the generated code (e.g., iframe + postMessage)
- Or constraining the output to a JSON spec interpreted by a fixed renderer (no `eval`-equivalent paths)
- Or routing generation through a CI step where humans review before merging

## Patterns that don't work

1. **Trying to load widgets at runtime via dynamic `import()`** — Vite needs static analysis to know about modules. `import.meta.glob` is the supported pattern; runtime `import('/widgets/foo/index.tsx')` won't tree-shake correctly and breaks production builds.
2. **Returning prose with the JSON** — the model loves to add "Here's your widget:" before the code block. The `cleaned.startswith("```")` strip + a one-shot example in the prompt keeps it consistent.
3. **Letting Claude pick the slug freely** — validate against a regex, or you'll get `../../etc/passwd` in `slug` from a prompt-injection attempt. The `SLUG_RE` check above is non-negotiable.

## Reference

In command-dash:
- `apps/web/src/lib/widget-registry.ts` — the glob loader
- `apps/web/src/components/WidgetCreator.tsx` — the prompt modal
- `apps/api/app/widgets/routes.py` — the `/api/widgets/generate` endpoint

