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" }