mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-23 21:25:33 -04:00
Cookbook model workflow fixes
This commit is contained in:
+155
-4
@@ -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 ? `` : '';
|
||||
}).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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user