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

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
  • #divBody flex 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.