# AI Page Skills Integration Plan

## Overview

Update `www/ai.html` to support Nostr skills (kind 31123). Skills define the LLM provider/model, system prompt template, temperature, max_tokens, and other execution parameters. Users can select **multiple skills** via checkboxes — their templates are concatenated into the system prompt in selection order. The last selected skill's execution params (llm, temperature, max_tokens, seed) win.

Skills that require tools (`requires_tool` tags) are shown but **dimmed**, since the ai.html page has no tool-calling capability. No nested skill resolution (`{{skill_d_tag}}`) in this version.

## Current Architecture

### Left Column (`divAiConversationsPane`)
- Title: "Conversations"
- Scrollable conversation list (`divAiConversationsList`)
- "+ New Chat" button at bottom

### Chat Header (`divAiChatHeader`)
- Provider selector (`selAiProvider`)
- Model dropdown (`ddAiModel`)
- Balance display (`divAiHeaderStats`)

### System Prompt Area
- `<details id="detailsAiPageSystemPrompt">` between thread and status
- Contains a textarea for a page-level system prompt
- Stored in localStorage (`ai_page_system_prompt_v1`)
- Used in `conversationToApiMessages()` via `getPageSystemPrompt()`

### Send Flow
1. `sendPrompt()` → `conversationToApiMessages()` → `callOpenAICompatibleChat()`
2. `conversationToApiMessages()` prepends system prompt from `getPageSystemPrompt()`
3. `callOpenAICompatibleChat()` uses `aiConfig.model`, `aiConfig.max_tokens`, `aiConfig.temperature`

## Target Architecture

### Left Column — Split Layout

```
┌─────────────────────────┐
│ Conversations            │  ← title
├─────────────────────────┤
│ ┌─────────────────────┐ │
│ │ Conversation 1      │ │  ← scrollable list, flex: 1
│ │ Conversation 2      │ │
│ │ ...                 │ │
│ └─────────────────────┘ │
│ [+ New Chat]            │
├─────────────────────────┤  ← divider
│ Skills                  │  ← title
├─────────────────────────┤
│ [All] [My]              │  ← filter tabs
├─────────────────────────┤
│ ┌─────────────────────┐ │
│ │ ☑ skill-slug-1      │ │  ← scrollable list with checkboxes
│ │ ☐ skill-slug-2      │ │     dimmed if requires_tool
│ │ ☑ skill-slug-3      │ │
│ │ ...                 │ │
│ └─────────────────────┘ │
│ [Clear All]             │  ← deselect all skills
└─────────────────────────┘
```

The left pane is split into two halves:
- **Top half**: Conversations (existing, unchanged)
- **Bottom half**: Skills selector with checkboxes (new)

Each half gets `flex: 1` so they share the vertical space equally.

### Multi-Skill Selection

- Each skill in the list has a **checkbox** for toggle selection
- Multiple skills can be selected simultaneously
- Skills with `requires_tool` tags are shown but **dimmed** (reduced opacity, tooltip explaining why)
- Dimmed skills can still be selected (user's choice) but may not work as intended
- A "Clear All" button deselects all skills
- Selection order is tracked (order of checkbox clicks)

### Chat Header — Simplified

Remove `selAiProvider` and `ddAiModel` from the header. Replace with:
- **Skill name(s) display**: Shows selected skill slugs (comma-separated), or "No skills" if none selected
- **Balance display**: Keep `divAiHeaderStats` as-is

When no skills are selected, the page falls back to the sidebar config (provider/model/etc.) as it does today. When skills ARE selected, the last selected skill's `llm` tag determines the model.

### Skill Area — Replaces System Prompt

Replace `<details id="detailsAiPageSystemPrompt">` with a stacked skill area:

```
┌─────────────────────────────────────────┐
│ ▶ Skills (3 selected)                   │  ← collapsed summary
└─────────────────────────────────────────┘

When expanded (▼):
┌─────────────────────────────────────────┐
│ ▼ Skills (3 selected)                   │
├─────────────────────────────────────────┤
│ ▶ skill-slug-1                          │  ← each skill is a nested details
│ ▼ skill-slug-2                          │
│   LLM: anthropic/claude-sonnet-4-20250514       │
│   Temp: 0.7  Tokens: 4000  Seed: 0     │
│   ┌─ Template ────────────────────────┐ │
│   │ system:                           │ │
│   │ You are a helpful assistant...    │ │
│   │ user:                             │ │
│   │ {{message}}                       │ │
│   └───────────────────────────────────┘ │
│ ▶ skill-slug-3                          │
├─────────────────────────────────────────┤
│ Fallback System Prompt:                 │
│ [textarea - used when no skills]        │
└─────────────────────────────────────────┘
```

When no skills are selected, this area shows the original system prompt textarea as a fallback.

### Context Assembly — Multi-Skill

When sending a message with multiple skills selected:

```mermaid
flowchart TD
    A[User clicks Send] --> B{Any skills selected?}
    B -->|Yes| C[Get selected skills in selection order]
    C --> D[For each skill: splitSkillTemplate]
    D --> E[Concatenate all system parts into one system message]
    E --> F[Resolve user template from LAST skill with user template]
    F --> G[Build messages: system + conversation history + user]
    G --> H[Get execution params from LAST selected skill]
    H --> I[callOpenAICompatibleChat with overrides]
    B -->|No| J[Use existing conversationToApiMessages]
    J --> K[callOpenAICompatibleChat with aiConfig defaults]
    I --> L[Display response in thread]
    K --> L
```

**Context assembly rules:**
1. Each selected skill's template is split via `splitSkillTemplate()` into `{ system, userTemplate }`
2. All `system` parts are concatenated (separated by `\n\n---\n\n`) in selection order
3. The `userTemplate` from the **last** selected skill that has one is used (with `{{message}}` resolved)
4. If no skill has a `userTemplate`, the raw user message is used
5. Execution params (`llm`, `temperature`, `max_tokens`, `seed`) come from the **last** selected skill

### Tool Requirements Dimming

Skills are parsed for `requires_tool` tags. Since ai.html has **zero tool-calling capability**:
- Any skill with one or more `requires_tool` tags is rendered with reduced opacity (0.5)
- A tooltip or subtitle shows "Requires tools: tool1, tool2"
- The checkbox is still functional — user can select dimmed skills if they want
- Skills with only `optional_tool` tags are NOT dimmed (optional means it works without)

## Detailed Changes

### 1. HTML Structure Changes

**Left column** (`divAiConversationsPane`):
- Wrap existing conversations section in a `divConversationsSection` div
- Add a new `divSkillsSection` div below with:
  - Skills title
  - Filter tabs (All / My)
  - Scrollable skills list (`divSkillsList`)
  - "Clear All" button

**Chat header** (`divAiChatHeader`):
- Remove `selAiProvider` select
- Remove `ddAiModel` dropdown (and hidden `selAiModel`)
- Add `divSkillHeaderLabel` span showing selected skill name(s)
- Keep `divAiHeaderStats`

**Skill area** (replaces `detailsAiPageSystemPrompt`):
- New `<details id="detailsSkillArea">` element
- Summary shows "Skills (N selected)" or "System Prompt" when none
- Body contains:
  - When skills selected: stacked `<details>` for each selected skill with editable fields
  - Always: fallback system prompt textarea at the bottom

### 2. CSS Changes

- `.aiConversationsSection` — top half of left pane
- `.aiSkillsSection` — bottom half of left pane
- Both get `flex: 1; min-height: 0; display: flex; flex-direction: column;`
- `.skillFilterTabs` — horizontal button row
- `.skillItem` — skill list item styling with checkbox
- `.skillItem.dimmed` — reduced opacity (0.5) for tool-requiring skills
- `.skillItem .skillCheckbox` — checkbox styling
- `.skillItem .skillRequiresTool` — small text showing required tools
- `.aiSkillControlsRow` — grid layout for inline fields
- `.aiSkillInlineField` — label + input pair
- `.aiTemplateDetails` — nested details for template
- `#divSkillHeaderLabel` — skill name(s) in header
- `.skillStackedItem` — each skill block in the skill area

### 3. JavaScript Changes

**New state variables:**
- `skills = []` — array of parsed skill objects
- `selectedSkillKeys = []` — ordered array of selected skill keys (maintains selection order)
- `skillFilterMode = 'all'` — filter mode for skills list
- `skillSubscription = null` — Nostr subscription for skills
- `skillSubId = null` — subscription ID for EOSE matching
- `authorProfiles = new Map()` — cached author profiles for skill display

**New constants:**
- `AVAILABLE_TOOLS = []` — empty array, no tools available in ai.html (future: add browser-capable tools)

**New functions (ported/adapted from html-tv.html):**
- `skillKey(pubkey, slug)` — composite key
- `normalizeSlug(raw)` — slug normalization
- `getTagValue(tags, name)` — tag value extraction
- `getAllTagValues(tags, name)` — get ALL values for a tag name (for `requires_tool`)
- `hasTagValue(tags, name, value)` — tag value check
- `parseSkillEvent(evt)` — parse kind 31123 event into skill object, extract `requires_tool` and `optional_tool` tags. **No `#m` filter** — show all skills.
- `splitSkillTemplate(template)` — split template into system/user parts
- `parseSkillTemplate(template, userInput)` — resolve template with user input
- `getSelectedSkills()` — get all selected skill objects in selection order
- `getEffectiveSkillParams()` — get execution params from last selected skill
- `renderSkillsList()` — render skills with checkboxes in left pane, dim tool-requiring ones
- `renderSkillArea()` — render stacked skill editors in the skill area
- `upsertSkillFromEvent(evt)` — add/update skill from event
- `refreshSkills()` — subscribe to kind 31123 events
- `clearSkillSubscription()` — cleanup subscription
- `bindSkillEventListeners()` — listen for `ndkEvent` (kind 31123) and `ndkEose`
- `buildMultiSkillSystemPrompt(selectedSkills, userMessage)` — concatenate skill templates into system + user messages

**Modified functions:**
- `conversationToApiMessages(convo)` — when skills selected, use `buildMultiSkillSystemPrompt()` instead of `getPageSystemPrompt()`
- `callOpenAICompatibleChat(messages, overrides)` — accept optional overrides object for `llm`, `temperature`, `max_tokens`, `seed` (pattern from html-tv.html)
- `sendPrompt()` — when skills selected, get effective params from last skill, pass overrides to `callOpenAICompatibleChat()`
- `bindUiEvents()` — add skill filter tab listeners, checkbox handlers, clear all button
- `main()` — call `refreshSkills()` and `bindSkillEventListeners()` after NDK init

**Multi-skill message building:**
When skills are selected:
1. Get selected skills in selection order from `selectedSkillKeys`
2. For each skill, call `splitSkillTemplate(skill.template)` → `{ system, userTemplate }`
3. Concatenate all `system` parts with `\n\n---\n\n` separator
4. Find the last skill with a non-empty `userTemplate`, resolve `{{message}}` with user input
5. Build messages: `[{ role: 'system', content: combinedSystem }, ...conversationHistory, { role: 'user', content: resolvedUser }]`
6. Get execution params from last selected skill: `{ llm, temperature, max_tokens, seed }`

When no skills are selected:
- Fall back to existing behavior (page system prompt + conversation history)

### 4. Skill Event Subscription

```javascript
const filter = {
  kinds: [31123],
  limit: 200
};
if (skillFilterMode === 'my' && currentPubkey) {
  filter.authors = [currentPubkey];
}
skillSubscription = subscribe(filter, {
  closeOnEose: false,
  cacheUsage: 'CACHE_FIRST'
});
```

No `#m` tag filter — show ALL kind 31123 skills regardless of output type.

### 5. Skill Parsing — requires_tool Extraction

```javascript
function parseSkillEvent(evt) {
  // ... existing parsing from html-tv.html ...
  // Additionally extract:
  skill.requires_tools = getAllTagValues(evt.tags, 'requires_tool');  // e.g. ['http_fetch', 'memory_read']
  skill.optional_tools = getAllTagValues(evt.tags, 'optional_tool');
  skill.requires_skills = getAllTagValues(evt.tags, 'requires_skill');
  skill.needsUnavailableTools = skill.requires_tools.length > 0;  // since AVAILABLE_TOOLS is empty
  // ...
}
```

### 6. Conversation Metadata

When skills are selected, store the skill keys in the conversation metadata so they can be restored when switching conversations:
- Add `skillKeys` field (array) to conversation object
- On conversation switch, if `convo.skillKeys` exists, restore those selections
- On skill selection change, update current conversation's `skillKeys`

## File Impact

| File | Change Type |
|------|-------------|
| `www/ai.html` | Major modification — HTML, CSS, and JS |

No new files needed. All changes are contained within `www/ai.html`.

## Key Design Decisions

1. **Multi-skill selection**: Users can select multiple skills via checkboxes. Templates are concatenated in selection order. Last skill's execution params win.

2. **No nested skill resolution**: `{{skill_d_tag}}` references are NOT resolved in this version. They remain as literal text in the template. Future enhancement.

3. **No `m` tag filter**: Unlike html-tv.html which filters for `text/html` skills, ai.html shows all skills since chat skills aren't necessarily HTML-output skills.

4. **Tool requirements dimming**: Skills with `requires_tool` tags are shown at reduced opacity since ai.html has zero tool-calling capability. Still selectable by user choice.

5. **Fallback to sidebar config**: When no skills are selected, the page works exactly as before — using the sidebar provider/model config and the system prompt textarea. This ensures backward compatibility.

6. **Editable skill fields**: The skill area allows editing all fields (template, temperature, etc.) for the current session. These edits are NOT saved back to Nostr. They only affect the current chat session.

7. **Provider/model from skill's `llm` tag**: The last selected skill's `llm` tag is passed as a model override. The base_url and api_key still come from the sidebar config (authentication credentials).

8. **Skill + conversation history**: Unlike html-tv.html which sends a single system+user pair, ai.html maintains conversation history. The combined skill templates provide the system prompt, and the conversation history is preserved.

9. **Selection order tracking**: `selectedSkillKeys` is an ordered array (not a Set) to preserve the order in which skills were checked. This order determines template concatenation order and which skill's params win (last).
