12 KiB
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 Skills —
authors: [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
dtag from same author: use latestcreated_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.