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()viagetPageSystemPrompt()
Send Flow
sendPrompt()→conversationToApiMessages()→callOpenAICompatibleChat()conversationToApiMessages()prepends system prompt fromgetPageSystemPrompt()callOpenAICompatibleChat()usesaiConfig.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_tooltags 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
divAiHeaderStatsas-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:
- Each selected skill's template is split via
splitSkillTemplate()into{ system, userTemplate } - All
systemparts are concatenated (separated by\n\n---\n\n) in selection order - The
userTemplatefrom the last selected skill that has one is used (with{{message}}resolved) - If no skill has a
userTemplate, the raw user message is used - 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_tooltags 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_tooltags are NOT dimmed (optional means it works without)
Detailed Changes
1. HTML Structure Changes
Left column (divAiConversationsPane):
- Wrap existing conversations section in a
divConversationsSectiondiv - Add a new
divSkillsSectiondiv below with:- Skills title
- Filter tabs (All / My)
- Scrollable skills list (
divSkillsList) - "Clear All" button
Chat header (divAiChatHeader):
- Remove
selAiProviderselect - Remove
ddAiModeldropdown (and hiddenselAiModel) - Add
divSkillHeaderLabelspan 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
- When skills selected: stacked
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 objectsselectedSkillKeys = []— ordered array of selected skill keys (maintains selection order)skillFilterMode = 'all'— filter mode for skills listskillSubscription = null— Nostr subscription for skillsskillSubId = null— subscription ID for EOSE matchingauthorProfiles = 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 keynormalizeSlug(raw)— slug normalizationgetTagValue(tags, name)— tag value extractiongetAllTagValues(tags, name)— get ALL values for a tag name (forrequires_tool)hasTagValue(tags, name, value)— tag value checkparseSkillEvent(evt)— parse kind 31123 event into skill object, extractrequires_toolandoptional_tooltags. No#mfilter — show all skills.splitSkillTemplate(template)— split template into system/user partsparseSkillTemplate(template, userInput)— resolve template with user inputgetSelectedSkills()— get all selected skill objects in selection ordergetEffectiveSkillParams()— get execution params from last selected skillrenderSkillsList()— render skills with checkboxes in left pane, dim tool-requiring onesrenderSkillArea()— render stacked skill editors in the skill areaupsertSkillFromEvent(evt)— add/update skill from eventrefreshSkills()— subscribe to kind 31123 eventsclearSkillSubscription()— cleanup subscriptionbindSkillEventListeners()— listen forndkEvent(kind 31123) andndkEosebuildMultiSkillSystemPrompt(selectedSkills, userMessage)— concatenate skill templates into system + user messages
Modified functions:
conversationToApiMessages(convo)— when skills selected, usebuildMultiSkillSystemPrompt()instead ofgetPageSystemPrompt()callOpenAICompatibleChat(messages, overrides)— accept optional overrides object forllm,temperature,max_tokens,seed(pattern from html-tv.html)sendPrompt()— when skills selected, get effective params from last skill, pass overrides tocallOpenAICompatibleChat()bindUiEvents()— add skill filter tab listeners, checkbox handlers, clear all buttonmain()— callrefreshSkills()andbindSkillEventListeners()after NDK init
Multi-skill message building: When skills are selected:
- Get selected skills in selection order from
selectedSkillKeys - For each skill, call
splitSkillTemplate(skill.template)→{ system, userTemplate } - Concatenate all
systemparts with\n\n---\n\nseparator - Find the last skill with a non-empty
userTemplate, resolve{{message}}with user input - Build messages:
[{ role: 'system', content: combinedSystem }, ...conversationHistory, { role: 'user', content: resolvedUser }] - 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
skillKeysfield (array) to conversation object - On conversation switch, if
convo.skillKeysexists, 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
-
Multi-skill selection: Users can select multiple skills via checkboxes. Templates are concatenated in selection order. Last skill's execution params win.
-
No nested skill resolution:
{{skill_d_tag}}references are NOT resolved in this version. They remain as literal text in the template. Future enhancement. -
No
mtag filter: Unlike html-tv.html which filters fortext/htmlskills, ai.html shows all skills since chat skills aren't necessarily HTML-output skills. -
Tool requirements dimming: Skills with
requires_tooltags are shown at reduced opacity since ai.html has zero tool-calling capability. Still selectable by user choice. -
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.
-
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.
-
Provider/model from skill's
llmtag: The last selected skill'sllmtag is passed as a model override. The base_url and api_key still come from the sidebar config (authentication credentials). -
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.
-
Selection order tracking:
selectedSkillKeysis 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).