Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce913997a | ||
|
|
63f5097a6d | ||
|
|
89fed0c818 |
+291
-12
@@ -334,6 +334,44 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docPreviewMeta {
|
||||
border: 1px solid var(--muted-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
background: color-mix(in srgb, var(--secondary-color) 88%, var(--muted-color) 12%);
|
||||
}
|
||||
|
||||
.docPreviewMetaRow {
|
||||
font-size: 84%;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.docPreviewMetaLabel {
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.docPreviewImageWrap {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.docPreviewImage {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--muted-color);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.docPreviewContent {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
#divDocumentInfo {
|
||||
font-size: 72%;
|
||||
color: var(--muted-color);
|
||||
@@ -911,6 +949,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 +958,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>
|
||||
{{document}}
|
||||
</document>
|
||||
|
||||
user:
|
||||
{{message}}`;
|
||||
|
||||
let updateIntervalId = null;
|
||||
let currentPubkey = null;
|
||||
@@ -1203,12 +1253,47 @@ Never include extra keys or text outside JSON.`;
|
||||
return window.DOMPurify ? window.DOMPurify.sanitize(html) : html;
|
||||
}
|
||||
|
||||
function normalizePreviewImageUrl(rawUrl) {
|
||||
const value = String(rawUrl || '').trim();
|
||||
if (!value) return '';
|
||||
try {
|
||||
const parsed = new URL(value, window.location.origin);
|
||||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
||||
return parsed.href;
|
||||
}
|
||||
} catch (_error) {
|
||||
// ignore invalid urls
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderDocumentMetaPreview() {
|
||||
const title = String(inpDocumentTitle?.value || '').trim();
|
||||
const summary = String(inpDocumentSummary?.value || '').trim();
|
||||
const tags = String(inpDocumentTags?.value || '').trim();
|
||||
const imageUrl = normalizePreviewImageUrl(inpDocumentImage?.value || '');
|
||||
const imageBlock = imageUrl
|
||||
? `<div class="docPreviewImageWrap"><img class="docPreviewImage" src="${escapeHtml(imageUrl)}" alt="Document image" loading="lazy" /></div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="docPreviewMeta">
|
||||
<div class="docPreviewMetaRow"><span class="docPreviewMetaLabel">Title:</span> ${escapeHtml(title || '—')}</div>
|
||||
<div class="docPreviewMetaRow"><span class="docPreviewMetaLabel">Summary:</span> ${escapeHtml(summary || '—')}</div>
|
||||
<div class="docPreviewMetaRow"><span class="docPreviewMetaLabel">Tags:</span> ${escapeHtml(tags || '—')}</div>
|
||||
${imageBlock}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateDocumentView() {
|
||||
if (!taDocument || !divDocumentPreview) return;
|
||||
if (documentViewMode === 'markdown') {
|
||||
taDocument.style.display = 'none';
|
||||
divDocumentPreview.style.display = 'block';
|
||||
divDocumentPreview.innerHTML = renderDocumentPreview(taDocument.value || '');
|
||||
const metaHtml = renderDocumentMetaPreview();
|
||||
const bodyHtml = renderDocumentPreview(taDocument.value || '');
|
||||
divDocumentPreview.innerHTML = `${metaHtml}<div class="docPreviewContent">${bodyHtml}</div>`;
|
||||
return;
|
||||
}
|
||||
taDocument.style.display = 'block';
|
||||
@@ -1260,6 +1345,7 @@ Never include extra keys or text outside JSON.`;
|
||||
title,
|
||||
modelId: '',
|
||||
skillKeys: [],
|
||||
removedSkillKeys: [],
|
||||
messages: [],
|
||||
createdAt: nowTs(),
|
||||
updatedAt: nowTs()
|
||||
@@ -1586,13 +1672,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 +1854,88 @@ 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 defaultKey = getDefaultDocumentSkillKey();
|
||||
if (!defaultKey) return;
|
||||
const hasDefaultSkill = skills.some((s) => s.key === defaultKey);
|
||||
if (!hasDefaultSkill) return;
|
||||
|
||||
const convo = getCurrentConversation();
|
||||
if (!convo) {
|
||||
if (!selectedSkillKeys.includes(defaultKey)) {
|
||||
selectedSkillKeys = [...selectedSkillKeys, defaultKey];
|
||||
renderSkillsList();
|
||||
renderSkillsEditor();
|
||||
}
|
||||
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 +1947,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 +2272,7 @@ Never include extra keys or text outside JSON.`;
|
||||
}
|
||||
renderSkillsList();
|
||||
renderSkillsEditor();
|
||||
enforceDefaultSkillSelection();
|
||||
}
|
||||
|
||||
function clearSkillSubscription() {
|
||||
@@ -2185,7 +2345,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 +2385,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 +2490,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 +3407,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;
|
||||
@@ -3430,6 +3599,7 @@ Never include extra keys or text outside JSON.`;
|
||||
inputEl.addEventListener('input', () => {
|
||||
if (CURRENT_NOTE) setDocumentInfo(`Editing draft • d:${CURRENT_NOTE} • UNSAVED`);
|
||||
scheduleDocumentAutosave();
|
||||
updateDocumentView();
|
||||
renderCurrentConversation();
|
||||
});
|
||||
inputEl.addEventListener('blur', () => {
|
||||
@@ -3465,6 +3635,7 @@ Never include extra keys or text outside JSON.`;
|
||||
if (btnSkillClearAll) {
|
||||
btnSkillClearAll.addEventListener('click', () => {
|
||||
selectedSkillKeys = [];
|
||||
setDefaultSkillExplicitlyRemoved(true);
|
||||
persistSelectedSkillsToConversation();
|
||||
renderSkillsList();
|
||||
renderSkillsEditor();
|
||||
@@ -3972,6 +4143,113 @@ Never include extra keys or text outside JSON.`;
|
||||
});
|
||||
}
|
||||
|
||||
function upsertDefaultDocumentSkillDraft() {
|
||||
if (!currentPubkey) return;
|
||||
const key = getDefaultDocumentSkillKey();
|
||||
if (!key) return;
|
||||
if (skills.some((s) => s.key === key)) return;
|
||||
|
||||
const llm = String(aiConfig.model || 'default').trim() || 'default';
|
||||
const temperature = Number(aiConfig.temperature ?? 0);
|
||||
const maxTokens = Math.max(1, Math.floor(Number(aiConfig.max_tokens ?? 10000)));
|
||||
const evt = {
|
||||
kind: 31123,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: currentPubkey,
|
||||
id: uid(),
|
||||
tags: [
|
||||
['d', DEFAULT_DOCUMENT_SKILL_SLUG],
|
||||
['description', DEFAULT_DOCUMENT_SKILL_DESCRIPTION],
|
||||
['llm', llm],
|
||||
['temperature', String(temperature)],
|
||||
['max_tokens', String(maxTokens)]
|
||||
],
|
||||
content: DEFAULT_DOCUMENT_SKILL_TEMPLATE
|
||||
};
|
||||
upsertSkillFromEvent(evt);
|
||||
}
|
||||
|
||||
async function ensureDefaultDocumentSkillExists() {
|
||||
if (!currentPubkey) return;
|
||||
const slug = DEFAULT_DOCUMENT_SKILL_SLUG;
|
||||
const key = getDefaultDocumentSkillKey();
|
||||
|
||||
// Ensure visibility/editability immediately, even before network round-trips.
|
||||
upsertDefaultDocumentSkillDraft();
|
||||
enforceDefaultSkillSelection();
|
||||
|
||||
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);
|
||||
try {
|
||||
await publishEvent(skillEvent);
|
||||
upsertSkillFromEvent({
|
||||
...skillEvent,
|
||||
pubkey: currentPubkey,
|
||||
id: uid()
|
||||
});
|
||||
setStatus('Created default-document-skill.');
|
||||
} catch (error) {
|
||||
console.warn('[document.html] failed to publish default-document-skill, keeping local draft:', error);
|
||||
setStatus('Default document skill loaded locally (publish pending).');
|
||||
}
|
||||
enforceDefaultSkillSelection();
|
||||
}
|
||||
|
||||
(async function main() {
|
||||
const versionInfo = await getVersion();
|
||||
console.log(`[document.html ${versionInfo.VERSION}] Loading...`);
|
||||
@@ -4104,6 +4382,7 @@ Never include extra keys or text outside JSON.`;
|
||||
renderSkillsList();
|
||||
renderSkillsEditor();
|
||||
refreshSkills();
|
||||
await ensureDefaultDocumentSkillExists();
|
||||
bindRelayActivityListeners();
|
||||
setStatus('Ready.');
|
||||
getRoutstrBalance().catch((error) => {
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.7.7",
|
||||
"VERSION_NUMBER": "0.7.7",
|
||||
"BUILD_DATE": "2026-04-28T14:00:02.302Z"
|
||||
"VERSION": "v0.7.10",
|
||||
"VERSION_NUMBER": "0.7.10",
|
||||
"BUILD_DATE": "2026-04-28T16:33:28.943Z"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user