# Skills Demo Page — Implementation Plan

## Overview

Transform [`www/skills-demo.html`](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`](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

```mermaid
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
```javascript
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
```javascript
{
  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
```mermaid
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
```mermaid
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
```mermaid
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:

```javascript
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`:

```javascript
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.
