Files
client/plans/skills-tv-page.md
2026-04-17 16:52:51 -04:00

12 KiB

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

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

{
  "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

// 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 Skillsauthors: [currentPubkey]
  • All Skills — no author filter

Skill Card 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():

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():

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.