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

16 KiB

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:

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

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

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).