|
|
|
|
@@ -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);
|
|
|
|
|
@@ -1215,12 +1253,47 @@ user:
|
|
|
|
|
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';
|
|
|
|
|
@@ -1803,13 +1876,21 @@ user:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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)) {
|
|
|
|
|
@@ -3518,6 +3599,7 @@ user:
|
|
|
|
|
inputEl.addEventListener('input', () => {
|
|
|
|
|
if (CURRENT_NOTE) setDocumentInfo(`Editing draft • d:${CURRENT_NOTE} • UNSAVED`);
|
|
|
|
|
scheduleDocumentAutosave();
|
|
|
|
|
updateDocumentView();
|
|
|
|
|
renderCurrentConversation();
|
|
|
|
|
});
|
|
|
|
|
inputEl.addEventListener('blur', () => {
|
|
|
|
|
@@ -4061,15 +4143,40 @@ user:
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
const existingInMemory = skills.find((s) => s.key === key);
|
|
|
|
|
if (existingInMemory) {
|
|
|
|
|
enforceDefaultSkillSelection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure visibility/editability immediately, even before network round-trips.
|
|
|
|
|
upsertDefaultDocumentSkillDraft();
|
|
|
|
|
enforceDefaultSkillSelection();
|
|
|
|
|
|
|
|
|
|
const filter = {
|
|
|
|
|
kinds: [31123],
|
|
|
|
|
@@ -4128,13 +4235,18 @@ user:
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const skillEvent = buildSkillEventFromEditor(draftSkill);
|
|
|
|
|
await publishEvent(skillEvent);
|
|
|
|
|
upsertSkillFromEvent({
|
|
|
|
|
...skillEvent,
|
|
|
|
|
pubkey: currentPubkey,
|
|
|
|
|
id: uid()
|
|
|
|
|
});
|
|
|
|
|
setStatus('Created default-document-skill.');
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|