diff --git a/www/document.html b/www/document.html
index 3f20b72..db2e3e0 100644
--- a/www/document.html
+++ b/www/document.html
@@ -911,6 +911,8 @@
const DOC_CHAT_D_PREFIX = 'doc-chat:';
const AVAILABLE_TOOLS = [];
const DEFAULT_SKILL_TEMPLATE = 'system:\nYou are a helpful assistant.\n\nuser:\n{{message}}';
+ const DEFAULT_DOCUMENT_SKILL_SLUG = 'default-document-skill';
+ const DEFAULT_DOCUMENT_SKILL_DESCRIPTION = 'Default skill for document chat/edit JSON contract.';
const NOTE_KINDS = [30023, 30024];
const DOCUMENT_EDITOR_WRAPPER_SYSTEM = `You are collaborating with the user on a single markdown document.
Return ONLY valid JSON with fields: {"chat": string, "document": string|null}.
@@ -918,6 +920,16 @@ Return ONLY valid JSON with fields: {"chat": string, "document": string|null}.
- document: full updated markdown document when making edits.
- If no edit is needed, set document to null.
Never include extra keys or text outside JSON.`;
+ const DEFAULT_DOCUMENT_SKILL_TEMPLATE = `system:
+${DOCUMENT_EDITOR_WRAPPER_SYSTEM}
+
+Current document:
+
+{{document}}
+
+
+user:
+{{message}}`;
let updateIntervalId = null;
let currentPubkey = null;
@@ -1260,6 +1272,7 @@ Never include extra keys or text outside JSON.`;
title,
modelId: '',
skillKeys: [],
+ removedSkillKeys: [],
messages: [],
createdAt: nowTs(),
updatedAt: nowTs()
@@ -1586,13 +1599,22 @@ Never include extra keys or text outside JSON.`;
};
}
- function parseSkillTemplate(template, userInput) {
+ function parseSkillTemplate(template, userInput, context = {}) {
const parts = splitSkillTemplate(template);
- const system = String(parts.system || '').trim();
+ const rawSystem = String(parts.system || '').trim();
const userTemplate = String(parts.userTemplate || '').trim();
+ const messageText = String(userInput || '').trim();
+ const documentText = String(context?.document || '').trim();
+ const system = rawSystem
+ .replaceAll('{{message}}', messageText)
+ .replaceAll('{{document}}', documentText)
+ .trim();
const user = userTemplate
- ? userTemplate.replaceAll('{{message}}', String(userInput || '').trim()).trim()
- : String(userInput || '').trim();
+ ? userTemplate
+ .replaceAll('{{message}}', messageText)
+ .replaceAll('{{document}}', documentText)
+ .trim()
+ : messageText;
return { system, user, userTemplate };
}
@@ -1759,27 +1781,80 @@ Never include extra keys or text outside JSON.`;
};
}
+ function getDefaultDocumentSkillKey() {
+ if (!currentPubkey) return '';
+ return skillKey(currentPubkey, DEFAULT_DOCUMENT_SKILL_SLUG);
+ }
+
+ function isDefaultDocumentSkillKey(key) {
+ return String(key || '').trim() === getDefaultDocumentSkillKey();
+ }
+
function getConversationSkillKeys(convo) {
return Array.isArray(convo?.skillKeys)
? convo.skillKeys.map((x) => String(x || '').trim()).filter(Boolean)
: [];
}
+ function getConversationRemovedSkillKeys(convo) {
+ return Array.isArray(convo?.removedSkillKeys)
+ ? convo.removedSkillKeys.map((x) => String(x || '').trim()).filter(Boolean)
+ : [];
+ }
+
+ function enforceDefaultSkillSelection() {
+ const convo = getCurrentConversation();
+ if (!convo) return;
+ const defaultKey = getDefaultDocumentSkillKey();
+ if (!defaultKey) return;
+ const hasDefaultSkill = skills.some((s) => s.key === defaultKey);
+ if (!hasDefaultSkill) return;
+
+ const removed = new Set(getConversationRemovedSkillKeys(convo));
+ const selected = new Set(getConversationSkillKeys(convo));
+ if (!removed.has(defaultKey) && !selected.has(defaultKey)) {
+ selected.add(defaultKey);
+ convo.skillKeys = Array.from(selected);
+ selectedSkillKeys = [...convo.skillKeys];
+ updateConversation({ skillKeys: [...convo.skillKeys] });
+ renderSkillsList();
+ renderSkillsEditor();
+ }
+ }
+
function syncSelectedSkillsFromConversation(convo) {
const target = getConversationSkillKeys(convo);
const curr = selectedSkillKeys.join('|');
const next = target.join('|');
- if (curr === next) return;
- selectedSkillKeys = target;
- renderSkillsList();
- renderSkillsEditor();
+ if (curr !== next) {
+ selectedSkillKeys = target;
+ renderSkillsList();
+ renderSkillsEditor();
+ }
+ enforceDefaultSkillSelection();
}
function persistSelectedSkillsToConversation() {
const convo = getCurrentConversation();
if (!convo) return;
convo.skillKeys = [...selectedSkillKeys];
- updateConversation({ skillKeys: [...selectedSkillKeys] });
+ convo.removedSkillKeys = getConversationRemovedSkillKeys(convo);
+ updateConversation({
+ skillKeys: [...selectedSkillKeys],
+ removedSkillKeys: [...convo.removedSkillKeys]
+ });
+ }
+
+ function setDefaultSkillExplicitlyRemoved(removed) {
+ const convo = getCurrentConversation();
+ if (!convo) return;
+ const defaultKey = getDefaultDocumentSkillKey();
+ if (!defaultKey) return;
+ const set = new Set(getConversationRemovedSkillKeys(convo));
+ if (removed) set.add(defaultKey);
+ else set.delete(defaultKey);
+ convo.removedSkillKeys = Array.from(set);
+ updateConversation({ removedSkillKeys: [...convo.removedSkillKeys] });
}
function toggleSkillSelection(key, enabled) {
@@ -1791,6 +1866,9 @@ Never include extra keys or text outside JSON.`;
} else if (!enabled && exists) {
selectedSkillKeys = selectedSkillKeys.filter((k) => k !== skillKeyRaw);
}
+ if (isDefaultDocumentSkillKey(skillKeyRaw)) {
+ setDefaultSkillExplicitlyRemoved(!enabled);
+ }
persistSelectedSkillsToConversation();
renderSkillsList();
renderSkillsEditor();
@@ -2113,6 +2191,7 @@ Never include extra keys or text outside JSON.`;
}
renderSkillsList();
renderSkillsEditor();
+ enforceDefaultSkillSelection();
}
function clearSkillSubscription() {
@@ -2185,7 +2264,9 @@ Never include extra keys or text outside JSON.`;
for (const skill of selected) {
const edit = getSkillEditorValue(skill);
- const parsed = parseSkillTemplate(edit.template, userInput);
+ const parsed = parseSkillTemplate(edit.template, userInput, {
+ document: String(taDocument?.value || '')
+ });
if (parsed.system) systemParts.push(parsed.system);
if (parsed.userTemplate) {
resolvedUser = parsed.user;
@@ -2223,6 +2304,7 @@ Never include extra keys or text outside JSON.`;
model_id: String(convo?.modelId || ''),
provider_name: String(aiConfig?.provider || ''),
skill_keys: Array.isArray(convo?.skillKeys) ? convo.skillKeys : [],
+ removed_skill_keys: getConversationRemovedSkillKeys(convo),
messages: deleted ? [] : safeMessages,
deleted: Boolean(deleted)
};
@@ -2327,6 +2409,9 @@ Never include extra keys or text outside JSON.`;
skillKeys: Array.isArray(parsed?.skill_keys)
? parsed.skill_keys.map((x) => String(x || '').trim()).filter(Boolean)
: [],
+ removedSkillKeys: Array.isArray(parsed?.removed_skill_keys)
+ ? parsed.removed_skill_keys.map((x) => String(x || '').trim()).filter(Boolean)
+ : [],
messages,
createdAt: Number(Date.parse(d) || nowTs()),
updatedAt: Number(parsed?.updated_at_ms || nowTs())
@@ -3241,7 +3326,10 @@ Never include extra keys or text outside JSON.`;
try {
const apiPayload = conversationToApiMessages(convo, { currentPrompt: prompt });
const wrapperSystem = buildDocumentSystemPrompt(prompt);
- const allMessages = [{ role: 'system', content: wrapperSystem }, ...apiPayload.messages];
+ const hasDefaultSkillSelected = getSelectedSkills().some((s) => normalizeSlug(s.slug) === DEFAULT_DOCUMENT_SKILL_SLUG);
+ const allMessages = hasDefaultSkillSelected
+ ? [...apiPayload.messages]
+ : [{ role: 'system', content: wrapperSystem }, ...apiPayload.messages];
let rawContent = '';
let responseJson = null;
@@ -3465,6 +3553,7 @@ Never include extra keys or text outside JSON.`;
if (btnSkillClearAll) {
btnSkillClearAll.addEventListener('click', () => {
selectedSkillKeys = [];
+ setDefaultSkillExplicitlyRemoved(true);
persistSelectedSkillsToConversation();
renderSkillsList();
renderSkillsEditor();
@@ -3972,6 +4061,83 @@ Never include extra keys or text outside JSON.`;
});
}
+ async function ensureDefaultDocumentSkillExists() {
+ if (!currentPubkey) return;
+ const slug = DEFAULT_DOCUMENT_SKILL_SLUG;
+ const key = getDefaultDocumentSkillKey();
+ const existingInMemory = skills.find((s) => s.key === key);
+ if (existingInMemory) {
+ enforceDefaultSkillSelection();
+ return;
+ }
+
+ const filter = {
+ kinds: [31123],
+ authors: [currentPubkey],
+ '#d': [slug],
+ limit: 1
+ };
+
+ const cached = await queryCache(filter).catch(() => []);
+ const cachedEvt = Array.isArray(cached) && cached.length > 0
+ ? [...cached].sort((a, b) => Number(b?.created_at || 0) - Number(a?.created_at || 0))[0]
+ : null;
+ if (cachedEvt) {
+ upsertSkillFromEvent(cachedEvt);
+ enforceDefaultSkillSelection();
+ return;
+ }
+
+ const relays = await ndkFetchEvents(filter).catch(() => []);
+ const relayEvt = Array.isArray(relays) && relays.length > 0
+ ? [...relays].sort((a, b) => Number(b?.created_at || 0) - Number(a?.created_at || 0))[0]
+ : null;
+ if (relayEvt) {
+ upsertSkillFromEvent(relayEvt);
+ enforceDefaultSkillSelection();
+ return;
+ }
+
+ const draftSkill = {
+ key,
+ slug,
+ description: DEFAULT_DOCUMENT_SKILL_DESCRIPTION,
+ template: DEFAULT_DOCUMENT_SKILL_TEMPLATE,
+ temperature: Number(aiConfig.temperature ?? 0),
+ max_tokens: Math.max(1, Math.floor(Number(aiConfig.max_tokens ?? 10000))),
+ llm: String(aiConfig.model || 'default').trim() || 'default',
+ seed: null,
+ requires_tools: [],
+ optional_tools: [],
+ requires_skills: [],
+ unavailable_required_tools: [],
+ needsUnavailableTools: false,
+ author: currentPubkey,
+ created_at: Math.floor(Date.now() / 1000),
+ raw: null
+ };
+
+ skillEditorValues[draftSkill.key] = {
+ slug: draftSkill.slug,
+ description: draftSkill.description,
+ llm: draftSkill.llm,
+ temperature: String(draftSkill.temperature),
+ max_tokens: String(draftSkill.max_tokens),
+ seed: '',
+ template: draftSkill.template
+ };
+
+ const skillEvent = buildSkillEventFromEditor(draftSkill);
+ await publishEvent(skillEvent);
+ upsertSkillFromEvent({
+ ...skillEvent,
+ pubkey: currentPubkey,
+ id: uid()
+ });
+ setStatus('Created default-document-skill.');
+ enforceDefaultSkillSelection();
+ }
+
(async function main() {
const versionInfo = await getVersion();
console.log(`[document.html ${versionInfo.VERSION}] Loading...`);
@@ -4104,6 +4270,7 @@ Never include extra keys or text outside JSON.`;
renderSkillsList();
renderSkillsEditor();
refreshSkills();
+ await ensureDefaultDocumentSkillExists();
bindRelayActivityListeners();
setStatus('Ready.');
getRoutstrBalance().catch((error) => {
diff --git a/www/js/version.json b/www/js/version.json
index 955467b..cd9d397 100644
--- a/www/js/version.json
+++ b/www/js/version.json
@@ -1,5 +1,5 @@
{
- "VERSION": "v0.7.7",
- "VERSION_NUMBER": "0.7.7",
- "BUILD_DATE": "2026-04-28T14:00:02.302Z"
+ "VERSION": "v0.7.8",
+ "VERSION_NUMBER": "0.7.8",
+ "BUILD_DATE": "2026-04-28T14:39:51.733Z"
}