11 KiB
Skills Demo Page — Implementation Plan
Overview
Transform www/skills-demo.html from a copy of the AI chat page into a skills demo page per the spec in plans/skills_demo_page.md.
The page reuses the existing shell (header, footer, sidenav with AI config + Routstr payments + relay section), the LLM calling infrastructure, and the styling framework. The main body content is completely replaced.
Architecture
graph TD
A[skills-demo.html] --> B[Shell - kept from ai.html]
A --> C[Main Body - NEW]
A --> D[Sidenav - kept from ai.html]
B --> B1[Header: SKILLS DEMO title]
B --> B2[Footer: relay status + pubkey]
C --> C1[Left Column 65%: Text Editor]
C --> C2[Right Column 35%: Skills Panel]
C1 --> C1a[Textarea with default text]
C1 --> C1b[Undo/Redo buttons]
C1 --> C1c[Status bar]
C2 --> C2a[Skills list - radio cards]
C2 --> C2b[Run Selected Skill button]
C2 --> C2c[Edit Skill button]
C2 --> C2d[New Skill button]
C2 --> C2e[Refresh Skills button]
C2 --> C2f[Skill Details section]
D --> D1[AI Config - unchanged]
D --> D2[Routstr Payments - unchanged]
D --> D3[Relay Section - unchanged]
D --> D4[Theme/Logout - unchanged]
A --> E[Skill Editor Modal - NEW]
E --> E1[Slug / Description / Template fields]
E --> E2[Temperature / Max Tokens inputs]
E --> E3[Save and Publish / Cancel buttons]
What to Keep from ai.html
HTML Structure
<head>: all CSS links, scripts, theme init- Header bar (
#divHeader) — change title text to "SKILLS DEMO" - Footer bar (
#divFooter) — unchanged - Sidenav (
#divSideNav) — unchanged, including:- OpenAI-Compatible Config section
- System Prompt section (can be hidden or repurposed)
- Routstr Payment Methods section
- Relay section
- Version bar with theme toggle and logout
CSS
- All
<style>rules for sidenav elements (.aiSideSection,.aiActionGroup,.aiInput,.aiTextarea, etc.) - All
<style>rules for Routstr invoice panels #divBodyflex override (change to skills layout)
JavaScript — Keep These Functions
uid(),nowTs(),escapeHtml(),setStatus()- All AI config management:
loadAiConfig(),saveAiConfigLocal(),saveAiConfigToUserSettings(),normalizeAiConfig(),mergeAiConfigFromSettings(),mergeProvidersPreservingSecrets(),getProviderEntries(),syncSelectedProviderProfile(),selectProvider(),renderProviderDropdown(),getOrderedModels(),renderModelList(),selectModel(),updateFavButton(),bindModelOptionClicks(),renderModelDropdown(),renderAiConfigForm(),applyAiConfigToHeader() - All Routstr functions:
getRoutstrBaseUrl(),getAuthToken(),refreshRoutstrWalletState(),pollLightningInvoiceStatus(), invoice/refund handlers - HTTP helpers:
normalizeBaseUrl(),getChatCompletionsUrl(),callOpenAICompatibleChat(),parseJsonSafe(),safeStringify(),toErrorDetails(),buildHttpError(),sleep() - Nav functions:
openNav(),closeNav(),toggleNav() - Footer/relay:
UpdateFooter(),Logout(),bindRelayActivityListeners() - Dropdown helpers:
setDropdownOpen(),closeAllCustomDropdowns()
JavaScript — Remove These Functions
- All conversation management:
loadConversations(),saveConversations(),createConversation(),updateConversation(),deleteConversation(),ensureConversationTitle(),renderConversationList(),renderCurrentConversation(),getConversationIndexById(),getCurrentConversation() - Chat message functions:
addMessage(),patchMessage(),conversationToApiMessages(),renderMarkdown(),formatMessageTime() sendPrompt(),setSendingState()
JavaScript — Remove These Variables
STORAGE_KEY,selectedConversationId,conversations,activeAssistantMessageId,requestCount,isSending
What to Add
New HTML — Main Body
Replace the #divAiLayout contents with:
#divSkillsLayout
├── #divTextEditorPane (65%)
│ ├── #taSkillsText (large textarea, pre-filled with default text)
│ ├── #divTextActions (Undo / Redo buttons)
│ └── #divSkillsStatus (status bar)
└── #divSkillsPanel (35%)
├── #divSkillsPanelTitle
├── #divSkillsList (scrollable skill cards)
├── #divSkillActions (Run / Edit / New / Refresh buttons)
└── #divSkillDetails (template, temperature, max_tokens display)
New HTML — Skill Editor Modal
#divSkillEditorOverlay (fixed overlay)
└── #divSkillEditorModal
├── #divSkillEditorTitle
├── form fields: slug, description, template, temperature, max_tokens
├── #btnSkillSavePublish (Save & Publish)
└── #btnSkillEditorCancel (Cancel)
New CSS
Page-specific styles for:
#divSkillsLayout— two-column flex layout#divTextEditorPane— 65% width, flex column#taSkillsText— large textarea, min-height 500px, monospace optional#divTextActions— button row for undo/redo#divSkillsStatus— status bar styling#divSkillsPanel— 35% width, bordered panel.skillCard— selectable card with radio-like behavior.skillCard.selected— highlighted state.skillCard.invalid— error badge state#divSkillDetails— collapsible details area#divSkillEditorOverlay/#divSkillEditorModal— modal overlay@media (max-width: 900px)— stack vertically
New JavaScript
State Variables
let skills = []; // Array of parsed skill objects
let selectedSkillSlug = null; // Currently selected skill d-tag
let textHistory = []; // Undo/redo stack
let historyIndex = -1; // Current position in history
let isRunning = false; // Skill execution in progress
let skillSubscription = null; // Nostr subscription handle
Skill Object Shape
{
slug: 'spellcheck', // d tag
description: 'Spelling...', // from description tag or content
template: 'system:\n...', // full template string
temperature: 0.1,
max_tokens: 4000,
author: '52a3e8...', // hex pubkey
raw: { /* original event */ }
}
New Functions
| Function | Purpose |
|---|---|
discoverSkills() |
Subscribe to kind 31123 with #t: skills-demo, listen for ndkEvent |
parseSkillEvent(evt) |
Parse a Nostr event into a skill object |
renderSkillsList() |
Render skill cards in the panel |
selectSkill(slug) |
Select a skill, update UI, show details |
renderSkillDetails() |
Show template/temp/max_tokens for selected skill |
runSelectedSkill() |
Parse template, call LLM, replace text |
parseSkillTemplate(template, userText) |
Split system/user, replace placeholder |
pushHistory(text) |
Push text to undo stack |
undo() |
Go back in history |
redo() |
Go forward in history |
updateUndoRedoButtons() |
Enable/disable undo/redo based on state |
openSkillEditor(skill) |
Open modal pre-filled with skill data |
openNewSkillEditor() |
Open modal with blank fields |
closeSkillEditor() |
Close modal |
saveAndPublishSkill() |
Build event, publish via publishEvent() |
buildSkillEvent(slug, desc, template, temp, maxTokens) |
Build kind 31123 event object |
refreshSkills() |
Close existing sub, re-subscribe |
setSkillsStatus(text) |
Update status bar |
Execution Flow
Page Load
sequenceDiagram
participant Page
participant NDK as NDK Worker
participant Relay as Nostr Relays
Page->>Page: Init shell, hamburger, sidenav bindings
Page->>NDK: initNDKPage
NDK-->>Page: Ready
Page->>Page: Load AI config from localStorage + user settings
Page->>Page: Init default text + push to history
Page->>Page: Render UI
Page->>NDK: subscribe kind 31123 + #t skills-demo
NDK->>Relay: REQ
Relay-->>NDK: Events
NDK-->>Page: ndkEvent for each skill
Page->>Page: Parse + add to skills array
Page->>Page: Render skills list
Note over Page: ndkEose arrives
Page->>Page: If no skills found, show message
Skill Execution
sequenceDiagram
participant User
participant Page
participant LLM as LLM Provider
User->>Page: Click Run Selected Skill
Page->>Page: Get selected skill + textarea text
Page->>Page: parseSkillTemplate - split system/user
Page->>Page: Push current text to undo history
Page->>Page: Set status Running skill: X...
Page->>LLM: callOpenAICompatibleChat with skill temp + max_tokens override
LLM-->>Page: Response text
Page->>Page: Replace textarea with response
Page->>Page: Set status Done
Page->>User: Updated text visible
Skill Publishing
sequenceDiagram
participant User
participant Editor as Skill Editor
participant NDK as NDK Worker
participant Relay as Nostr Relays
User->>Editor: Fill in fields, click Save and Publish
Editor->>Editor: Validate fields
Editor->>Editor: buildSkillEvent - kind 31123
Editor->>NDK: publishEvent
NDK->>NDK: Sign event
NDK->>Relay: Publish
Relay-->>NDK: OK
NDK-->>Editor: Success
Editor->>Editor: Close modal
Editor->>Editor: Add/update skill in local list
Editor->>Editor: Re-render skills
LLM Call Override
The existing callOpenAICompatibleChat() uses aiConfig.temperature and aiConfig.max_tokens. For skill execution, we need to temporarily override these with the skill's values. The approach:
async function runSelectedSkill() {
const skill = skills.find(s => s.slug === selectedSkillSlug);
if (!skill) return;
const text = taSkillsText.value;
if (!text.trim()) return;
// Parse template
const parsed = parseSkillTemplate(skill.template, text);
// Save current text to history
pushHistory(text);
// Temporarily override config for this call
const savedTemp = aiConfig.temperature;
const savedMaxTokens = aiConfig.max_tokens;
aiConfig.temperature = skill.temperature ?? 0.7;
aiConfig.max_tokens = skill.max_tokens ?? 2000;
try {
isRunning = true;
setSkillsStatus('Running skill: ' + skill.slug + '...');
const startTime = Date.now();
const result = await callOpenAICompatibleChat([
{ role: 'system', content: parsed.system },
{ role: 'user', content: parsed.user }
]);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
taSkillsText.value = result.content;
pushHistory(result.content);
setSkillsStatus('Done (' + elapsed + 's)');
} catch (error) {
setSkillsStatus('Error: ' + (error.message || error));
} finally {
// Restore config
aiConfig.temperature = savedTemp;
aiConfig.max_tokens = savedMaxTokens;
isRunning = false;
}
}
Import Changes
Add publishEvent to the imports from init-ndk.mjs:
import {
initNDKPage,
getPubkey,
disconnect,
getVersion,
updateVersionDisplay,
getUserSettings,
patchUserSettings,
onUserSettings,
subscribe,
publishEvent // NEW
} from './js/init-ndk.mjs';
File Changes Summary
Only one file is modified: www/skills-demo.html
No new files are created. No existing files are modified.
The change is a single-file transformation of the copied ai.html into the skills demo page.