Cookbook model workflow fixes

This commit is contained in:
pewdiepie-archdaemon
2026-06-21 11:02:35 +00:00
parent 8c46172e87
commit c504214925
38 changed files with 3042 additions and 459 deletions
+155 -4
View File
@@ -24,6 +24,7 @@ import * as Modals from './modalManager.js';
let _autoDetectDebounce = null;
let _autoTitleDebounce = null;
let _autoSaveDebounce = null;
let _lastAutoSaveErrorAt = 0;
let _animationInProgress = false;
let _animationCancel = null; // function to cancel current animation
let _htmlPreviewActive = false; // true when inline HTML preview iframe is showing
@@ -153,6 +154,20 @@ import * as Modals from './modalManager.js';
addDocToTabs,
syncDocIndicator: _syncDocIndicator,
});
const sidebarNewDocBtn = document.getElementById('library-new-doc-btn');
if (sidebarNewDocBtn && !sidebarNewDocBtn.dataset.docNewWired) {
sidebarNewDocBtn.dataset.docNewWired = '1';
sidebarNewDocBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
try {
await newDocument();
} catch (err) {
console.error('Failed to create document from sidebar button:', err);
if (uiModule) uiModule.showError('Failed to create document');
}
});
}
_maybeOpenDocFromHash();
window.addEventListener('hashchange', _maybeOpenDocFromHash);
}
@@ -2685,6 +2700,104 @@ import * as Modals from './modalManager.js';
await _uploadComposeFiles(files);
}
function _isMarkdownImageFile(file) {
if (!file) return false;
if ((file.type || '').toLowerCase().startsWith('image/')) return true;
return /\.(avif|bmp|gif|jpe?g|png|svg|webp)$/i.test(file.name || '');
}
function _markdownImageAlt(name) {
const base = String(name || 'image').replace(/\.[^.]+$/, '').trim() || 'image';
return base.replace(/[\[\]\n\r]/g, ' ').replace(/\s+/g, ' ').trim() || 'image';
}
function _activeDocLanguage() {
const doc = activeDocId && docs.get(activeDocId);
return ((doc && doc.language) || document.getElementById('doc-language-select')?.value || '').toLowerCase();
}
function _scheduleMarkdownImageAutosave(ta) {
updateLineNumbers(ta.value);
const codeEl = document.getElementById('doc-editor-code');
if (codeEl && !codeEl.dataset.hasDiff) {
codeEl.textContent = ta.value + '\n';
codeEl.style.minHeight = ta.scrollHeight + 'px';
}
clearTimeout(_hlDebounce);
_hlDebounce = setTimeout(syncHighlighting, 80);
clearTimeout(_autoTitleDebounce);
_autoTitleDebounce = setTimeout(() => autoTitleFromContent(ta.value), 600);
clearTimeout(_autoSaveDebounce);
_autoSaveDebounce = setTimeout(() => { saveDocument({ silent: true }); }, 800);
}
function _insertMarkdownImages(uploadedFiles) {
const ta = document.getElementById('doc-editor-textarea');
if (!ta) return;
const files = Array.isArray(uploadedFiles) ? uploadedFiles : [];
if (!files.length) return;
const start = ta.selectionStart || 0;
const end = ta.selectionEnd || start;
const before = ta.value.slice(0, start);
const after = ta.value.slice(end);
const lines = files.map(file => {
const id = encodeURIComponent(file.id || file.file_id || '');
const alt = _markdownImageAlt(file.name || file.filename);
return id ? `![${alt}](/api/upload/${id})` : '';
}).filter(Boolean);
if (!lines.length) return;
const prefix = before && !before.endsWith('\n') ? '\n' : '';
const suffix = after && !after.startsWith('\n') ? '\n' : '';
const insert = `${prefix}${lines.join('\n\n')}${suffix}`;
_replaceRange(ta, start, end, insert);
const caret = start + insert.length;
ta.selectionStart = caret;
ta.selectionEnd = caret;
ta.focus();
_scheduleMarkdownImageAutosave(ta);
_refreshMarkdownPreviewIfVisible(activeDocId, ta.value);
}
async function _uploadMarkdownImages(files) {
const images = Array.from(files || []).filter(_isMarkdownImageFile);
if (!images.length) {
if (uiModule) uiModule.showError('Choose an image file');
return;
}
if (_activeDocLanguage() !== 'markdown') {
if (uiModule) uiModule.showError('Switch the document to markdown before inserting images');
return;
}
const fd = new FormData();
images.forEach(file => fd.append('files', file));
try {
const res = await fetch(`${API_BASE}/api/upload`, {
method: 'POST',
credentials: 'same-origin',
body: fd,
});
let data = null;
try { data = await res.json(); } catch (_) {}
if (!res.ok) throw new Error((data && (data.error || data.detail)) || `HTTP ${res.status}`);
const uploaded = Array.isArray(data?.files) ? data.files : [];
if (!uploaded.length) throw new Error('No uploaded files returned');
_insertMarkdownImages(uploaded);
if (uiModule) uiModule.showToast(images.length === 1 ? 'Image inserted' : 'Images inserted');
} catch (err) {
console.error('Failed to insert markdown image:', err);
if (uiModule) uiModule.showError('Failed to insert image');
}
}
async function _handleMarkdownImageUpload(e) {
const files = e.target.files;
e.target.value = '';
await _uploadMarkdownImages(files);
}
function _renderComposeAttachments() {
const container = document.getElementById('doc-email-compose-atts');
if (!container) return;
@@ -3751,9 +3864,12 @@ import * as Modals from './modalManager.js';
const res = await fetch(`${API_BASE}/api/document`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ session_id: sessionId, title: '', content }),
});
if (!res.ok) throw new Error(`Document create failed: HTTP ${res.status}`);
const doc = await res.json();
if (!doc || !doc.id) throw new Error('Document create failed: missing id');
addDocToTabs(doc, sessionId);
// Set the content into the map so switchToDoc preserves it
const d = docs.get(doc.id);
@@ -3980,6 +4096,7 @@ import * as Modals from './modalManager.js';
<input type="hidden" id="doc-email-source-folder" />
<input type="file" id="doc-email-file-input" multiple style="display:none" />
</div>
<input type="file" id="doc-md-image-input" accept="image/*" multiple style="display:none" />
<div class="doc-md-toolbar" id="doc-md-toolbar" style="display:none">
<div class="md-toolbar-items" id="md-toolbar-items">
<span class="md-view-toggle" id="doc-md-view-toggle" style="display:none" role="group" aria-label="Edit or preview">
@@ -4002,7 +4119,7 @@ import * as Modals from './modalManager.js';
<button type="button" class="md-dd-toggle" data-dd="list" title="List"><span style="font-variant-numeric:tabular-nums;">1.</span><svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button>
<span class="md-toolbar-sep"></span>
<button type="button" data-md="link" title="Link"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
<button type="button" id="md-toolbar-attach-btn" class="md-toolbar-attach-btn" title="Attach files"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 17.93 8.8l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></button>
<button type="button" id="md-toolbar-attach-btn" class="md-toolbar-attach-btn" title="Insert image"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 17.93 8.8l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></button>
<button type="button" class="md-dd-toggle md-toolbar-email-hide" data-dd="code" title="Code">\`<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button>
<button type="button" data-md="hr" title="Horizontal rule"></button>
<span class="md-toolbar-sep"></span>
@@ -4601,9 +4718,14 @@ import * as Modals from './modalManager.js';
document.getElementById('doc-email-file-input')?.click();
});
document.getElementById('md-toolbar-attach-btn')?.addEventListener('click', () => {
document.getElementById('doc-email-file-input')?.click();
if (_activeDocLanguage() === 'email') {
document.getElementById('doc-email-file-input')?.click();
} else {
document.getElementById('doc-md-image-input')?.click();
}
});
document.getElementById('doc-email-file-input')?.addEventListener('change', _handleAttachUpload);
document.getElementById('doc-md-image-input')?.addEventListener('change', _handleMarkdownImageUpload);
// Cc/Bcc toggle
document.getElementById('doc-email-show-cc')?.addEventListener('click', () => {
@@ -4839,6 +4961,26 @@ import * as Modals from './modalManager.js';
clearTimeout(_autoSaveDebounce);
_autoSaveDebounce = setTimeout(() => { saveDocument({ silent: true }); }, 2000);
});
ta.addEventListener('paste', (e) => {
if (_activeDocLanguage() !== 'markdown') return;
const files = Array.from(e.clipboardData?.files || []).filter(_isMarkdownImageFile);
if (!files.length) return;
e.preventDefault();
_uploadMarkdownImages(files);
});
ta.addEventListener('dragover', (e) => {
if (_activeDocLanguage() !== 'markdown') return;
const items = Array.from(e.dataTransfer?.items || []);
if (!items.some(item => item.kind === 'file' && /^image\//i.test(item.type || ''))) return;
e.preventDefault();
});
ta.addEventListener('drop', (e) => {
if (_activeDocLanguage() !== 'markdown') return;
const files = Array.from(e.dataTransfer?.files || []).filter(_isMarkdownImageFile);
if (!files.length) return;
e.preventDefault();
_uploadMarkdownImages(files);
});
ta.addEventListener('scroll', () => {
const code = document.getElementById('doc-editor-code');
if (code) code.style.minHeight = ta.scrollHeight + 'px';
@@ -5547,7 +5689,7 @@ import * as Modals from './modalManager.js';
// any dropdown that just opened. Preventing the default mousedown keeps the
// textarea focused, so formatting hits the live selection and menus stay up.
toolbar.addEventListener('mousedown', (e) => {
if (e.target.closest('[data-md], .md-dd-toggle, .emoji-picker-btn')) e.preventDefault();
if (e.target.closest('[data-md], .md-dd-toggle, .emoji-picker-btn, .md-toolbar-attach-btn')) e.preventDefault();
});
toolbar.addEventListener('click', (e) => {
@@ -5975,6 +6117,7 @@ import * as Modals from './modalManager.js';
const res = await fetch(`${API_BASE}/api/document`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({
session_id: sessionId,
title: '',
@@ -5982,7 +6125,9 @@ import * as Modals from './modalManager.js';
language: 'markdown',
}),
});
if (!res.ok) throw new Error(`Document create failed: HTTP ${res.status}`);
const doc = await res.json();
if (!doc || !doc.id) throw new Error('Document create failed: missing id');
addDocToTabs(doc, sessionId);
if (!isOpen) openPanel();
// Re-enable editor if it was in empty state
@@ -8265,8 +8410,10 @@ import * as Modals from './modalManager.js';
const res = await fetch(`${API_BASE}/api/document/${activeDocId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ content: textarea.value }),
});
if (!res.ok) throw new Error(`Document save failed: HTTP ${res.status}`);
const doc = await res.json();
const badge = document.getElementById('doc-version-badge');
if (badge) { const _v = doc.version_count || 1; badge.textContent = `v${_v}`; badge.style.display = _v > 1 ? '' : 'none'; }
@@ -8279,7 +8426,11 @@ import * as Modals from './modalManager.js';
if (!silent && uiModule) uiModule.showToast('Document saved');
} catch (e) {
console.error('Failed to save document:', e);
if (!silent && uiModule) uiModule.showError('Failed to save document');
const now = Date.now();
if (uiModule && (!silent || now - _lastAutoSaveErrorAt > 10000)) {
uiModule.showError(silent ? 'Autosave failed' : 'Failed to save document');
_lastAutoSaveErrorAt = now;
}
}
}