# HTML TV Page Architecture Plan

## Concept

**HTML TV** is a Nostr-native skill browser and executor that renders LLM output as full HTML pages. Instead of freeform prompts, users create, discover, and run **Nostr skills** (kind 31123) that produce HTML output. Skills are filtered by the `["m", "text/html"]` tag, meaning any skill that declares its output MIME type as HTML is discoverable here.

The page combines:
- A **skills browser** (left sidebar) for discovering and selecting skills from the network
- An **iframe viewer** (center) for rendering LLM-generated HTML pages
- A **skill editor** (bottom) for creating/editing skill templates and running them

## Layout

```
┌─────────────────────────────────────────────────────────────┐
│  Header: [Provider ▼] [Model ▼]                   Balance   │
├──────────────┬──────────────────────────────────────────────┤
│              │                                              │
│  Skills      │           iframe TV Screen                   │
│  Browser     │                                              │
│              │   sandboxed iframe rendering                 │
│  [My | All]  │   the LLM HTML output                       │
│              │                                              │
│  ┌────────┐  │   [Save to Blossom] [Launch]                 │
│  │ skill1 │  │                                              │
│  │ author  │  │                                              │
│  └────────┘  │                                              │
│  ┌────────┐  │                                              │
│  │ skill2 │  │                                              │
│  └────────┘  │                                              │
│              │                                              │
│  [+ New]     │                                              │
├──────────────┴──────────────────────────────────────────────┤
│  ▶ Skill Editor (collapsible)                               │
│  [slug: ________] [description: ________________]           │
│  ┌──────────────────────────────────────────────────────┐   │
│  │ system:\nYou are a web page generator...\nuser:\n... │   │
│  └──────────────────────────────────────────────────────┘   │
│  [temp: 0.7] [max_tokens: 4000]                             │
│  [User input: "make a dashboard about Bitcoin prices"]      │
│  [Run & Save]                                    [Delete]   │
├─────────────────────────────────────────────────────────────┤
│  Status bar                                                 │
└─────────────────────────────────────────────────────────────┘
```

## Architecture Diagram

```mermaid
graph TD
    A[HTML TV Page] --> B[Header Bar]
    A --> C[Main Content Area]
    A --> D[Skill Editor Section]
    A --> E[Footer]

    B --> B1[Provider Select]
    B --> B2[Model Dropdown]
    B --> B3[Balance Display]

    C --> C1[Skills Browser - left sidebar]
    C --> C2[iframe TV Screen - main area]

    C1 --> C1a["Filter tabs: My Skills / All Skills"]
    C1 --> C1b["Subscribe: kinds 31123, #m text/html"]
    C1 --> C1c[Skill cards with author avatar]
    C1 --> C1d[Click to load skill into editor]
    C1 --> C1e[+ New Skill button]

    C2 --> C2a["sandbox=allow-scripts"]
    C2 --> C2b["srcdoc set from LLM response"]
    C2 --> C2c[Blossom save/launch per result]

    D --> D1[Collapsible skill editor panel]
    D --> D2["Fields: slug, description, template textarea"]
    D --> D3["Params: temperature, max_tokens"]
    D --> D4["User input field for {{message}}"]
    D --> D5["Run and Save button"]
    D --> D6["Delete button - own skills only"]

    subgraph Execution Flow
        R1[User clicks Run and Save] --> R2[Parse skill template into system+user messages]
        R2 --> R3[Replace {{message}} with user input]
        R3 --> R4[Call LLM via OpenAI-compatible API]
        R4 --> R5[extractHtmlFromResponse]
        R5 --> R6[Set iframe.srcdoc]
        R1 --> R7[Publish kind 31123 event to Nostr]
    end
```

## Nostr Skill Event Structure

### Publishing a Skill

```json
{
  "kind": 31123,
  "content": "{\"description\":\"...\",\"context_mode\":\"full\",\"llm\":\"default\",\"tools\":false,\"max_tokens\":4000,\"temperature\":0.7,\"template\":\"system:\\n...\\nuser:\\n{{message}}\"}",
  "tags": [
    ["d", "retro-dashboard"],
    ["m", "text/html"],
    ["scope", "public"],
    ["description", "Generate a retro-styled dashboard"]
  ],
  "created_at": 1234567890
}
```

### Subscribing to Skills

```javascript
// All HTML-output skills on the network
subscribe({
  kinds: [31123],
  '#m': ['text/html'],
  limit: 50
}, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' });

// Only my skills
subscribe({
  kinds: [31123],
  '#m': ['text/html'],
  authors: [currentPubkey],
  limit: 50
}, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' });
```

## Detailed Component Specifications

### 1. Skills Browser (Left Sidebar)

**Filter Tabs:**
- **My Skills** — `authors: [currentPubkey]`
- **All Skills** — no author filter

**Skill Card HTML:**
```html
<div class="skillCard selected" data-skill-slug="retro-dashboard" data-skill-author="abc123">
  <div class="skillCardHeader">
    <div class="skillCardTitle">retro-dashboard</div>
    <img class="skillAuthorAvatar" src="..." />
  </div>
  <div class="skillCardMeta">Generate a retro-styled dashboard</div>
  <div class="skillCardMeta">author-name</div>
</div>
```

**Behavior:**
- Clicking a card loads the skill into the editor panel
- Selected card is highlighted
- Author profiles are lazily fetched via `fetchCachedProfile()`
- Skills are sorted alphabetically by slug
- Duplicate slugs from same author are deduplicated (latest event wins)

### 2. iframe TV Screen (Center)

Reused directly from ai-tv.html:
- `<iframe id="iframeAiTv" sandbox="allow-scripts">`
- `extractHtmlFromResponse()` strips code fences
- Loading overlay during LLM call
- Blossom save button + launch link per rendered page

### 3. Skill Editor Panel (Bottom)

**Fields:**
| Field | Element | Description |
|-------|---------|-------------|
| Slug | `<input id="inpSkillSlug">` | The `d` tag value, URL-safe identifier |
| Description | `<input id="inpSkillDescription">` | Human-readable description |
| Template | `<textarea id="taSkillTemplate">` | The full skill template with `system:` and `user:` sections |
| Temperature | `<input id="inpSkillTemperature" type="number">` | 0-2, default 0.7 |
| Max Tokens | `<input id="inpSkillMaxTokens" type="number">` | Default 4000 |
| User Input | `<textarea id="taSkillUserInput">` | Replaces `{{message}}` in template |

**Buttons:**
- **Run & Save** — Executes the skill against the LLM AND publishes as kind 31123
- **Delete** — Only enabled for user-owned skills, publishes kind 5 deletion event

### 4. Default Skill Template

When creating a new skill, pre-populate with:

```
system:
You are a web page generator. You must respond to ALL requests with a complete, valid HTML page. Your response must be ONLY the HTML — no markdown, no explanation, no code fences. The page should be self-contained with inline CSS and JavaScript as needed.

user:
{{message}}
```

### 5. Template Parsing

Reuse the pattern from `skills-demo.mjs` `parseSkillTemplate()`:

```javascript
function parseSkillTemplate(template, userInput) {
  const raw = String(template || '');
  const marker = '\nuser:\n';
  const idx = raw.indexOf(marker);
  if (idx === -1) {
    return {
      system: raw.replace(/^system:\n/, '').trim(),
      user: String(userInput || '')
    };
  }
  let systemPart = raw.substring(0, idx).replace(/^system:\n/, '').trim();
  let userPart = raw.substring(idx + marker.length)
    .replace('{{message}}', String(userInput || '')).trim();
  return { system: systemPart, user: userPart };
}
```

### 6. Skill Event Building

Reuse the pattern from `skills-demo.mjs` `buildSkillEvent()`:

```javascript
function buildSkillEvent(slug, description, template, temperature, maxTokens) {
  return {
    kind: 31123,
    content: JSON.stringify({
      description,
      context_mode: 'full',
      llm: 'default',
      tools: false,
      max_tokens: maxTokens,
      temperature,
      template
    }),
    tags: [
      ['d', slug],
      ['m', 'text/html'],
      ['scope', 'public'],
      ['description', description]
    ],
    created_at: Math.floor(Date.now() / 1000)
  };
}
```

## What Gets Reused from ai-tv.html

| Component | Reuse | Notes |
|-----------|-------|-------|
| Header bar (provider/model) | ✅ Copy | Same LLM config UI |
| iframe viewer + loading overlay | ✅ Copy | Core rendering unchanged |
| `extractHtmlFromResponse()` | ✅ Copy | Strip code fences, validate HTML |
| `callOpenAICompatibleChat()` | ✅ Copy | Same API call pattern |
| Blossom save/launch | ✅ Copy | `saveAssistantPageToBlossom()` |
| Sidebar config (API keys, payment) | ✅ Copy | Same sidebar nav |
| Footer, relay status, blossom section | ✅ Copy | Standard page chrome |
| Past Pages strip | ❌ Replace | Becomes Skills Browser |
| System prompt section | ❌ Replace | Becomes Skill Editor |
| Conversation localStorage | ❌ Replace | Skills come from Nostr |
| `sendPrompt()` | 🔄 Adapt | Parse template + publish event |

## What Gets Reused from skills-demo.mjs

| Function | Reuse | Notes |
|----------|-------|-------|
| `parseSkillEvent()` | ✅ Adapt | Remove DEMO_TAG filter, add `m` tag check |
| `parseSkillTemplate()` | ✅ Copy | System/user message parsing |
| `buildSkillEvent()` | ✅ Adapt | Add `["m", "text/html"]` tag |
| `renderSkillsList()` | ✅ Adapt | Skill card rendering pattern |
| `lookupAuthorProfile()` | ✅ Copy | Author avatar/name resolution |
| `refreshSkills()` / subscribe pattern | ✅ Adapt | Change filter to `#m: ['text/html']` |
| `deleteSkill()` | ✅ Copy | Kind 5 deletion event |
| `saveAndPublishSkill()` | ✅ Adapt | Publish + refresh |

## Files to Create/Modify

| File | Action |
|------|--------|
| `www/html-tv.html` | **Create** — new page, based on ai-tv.html structure |
| `www/index.html` | **Modify** — add html-tv tile to navigation |
| `plans/html-tv-page.md` | **Create** — this plan |

## Risk Considerations

- **Large skill templates in Nostr events** — Skill content is JSON-stringified in the event content field. Templates with long system prompts could be large but should be fine for relay limits.
- **Skill deduplication** — Multiple events with same `d` tag from same author: use latest `created_at`.
- **Author profile loading** — Lazy-load profiles to avoid blocking the skill list render.
- **Concurrent save + run** — The "Run & Save" button does both in parallel; if publish fails but LLM succeeds, the page still renders but the skill isn't saved. Show appropriate status.
