feat: loading AI + tooltips + auto-save silencieux + hover transparent fix
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Successful in 9s

This commit is contained in:
Bruno Charest 2026-05-30 21:35:35 -04:00
parent a662a4a3c2
commit adbeb54887
2 changed files with 52 additions and 29 deletions

View File

@ -96,6 +96,8 @@ function createMenu(items, parentEl) {
fontSize: '0.8rem', fontSize: '0.8rem',
color: 'var(--text-primary)', color: 'var(--text-primary)',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
background: 'transparent',
transition: 'background 0.1s',
}); });
el.innerHTML = item.label; el.innerHTML = item.label;
if (item.hint) { if (item.hint) {
@ -230,35 +232,41 @@ export async function createAIToolbar(container, getEditorView) {
padding: '3px 8px', padding: '3px 8px',
borderRadius: '4px', borderRadius: '4px',
}); });
aiBtn.title = 'AI Inline Completion (Ctrl+J)'; aiBtn.title = 'AI Inline Completion (Ctrl+J) — Complète le texte automatiquement';
aiBtn.addEventListener('click', async () => { aiBtn.addEventListener('click', async () => {
const v = ev(); const v = ev();
if (!v) return; if (!v) return;
const text = getSelection(v); const text = getSelection(v);
if (!text.trim()) return; if (!text.trim()) return;
const origHTML = aiBtn.innerHTML;
aiBtn.innerHTML = '⏳ AI...';
aiBtn.disabled = true;
try { try {
const result = await aiAction('inline-complete', text); const result = await aiAction('inline-complete', text);
replaceSelection(v, result, 'append'); replaceSelection(v, result, 'append');
showToast('AI: complétion ajoutée', 'success'); showToast('AI: complétion ajoutée', 'success');
} catch (e) { } catch (e) {
showToast('AI: ' + (String(e.message || e).includes('401') ? 'clé API invalide' : e.message), 'error'); showToast('AI: ' + (String(e.message || e).includes('401') ? 'clé API invalide' : e.message), 'error');
} finally {
aiBtn.innerHTML = origHTML;
aiBtn.disabled = false;
} }
}); });
// ── Edit menu ── // ── Edit menu ──
const editBtn = createDropdownBtn('Éditer', [ const editBtn = createDropdownBtn('Éditer', [
{ label: '🪄 Improve writing', action: () => action('improve') }, { label: '🪄 Improve writing', action: () => action('improve'), hint: 'Améliore la qualité du texte' },
{ label: '🔤 Fix spelling & grammar', action: () => action('fix-spelling') }, { label: '🔤 Fix spelling & grammar', action: () => action('fix-spelling'), hint: 'Corrige les fautes' },
{ label: '📏 Make shorter', action: () => action('make-shorter') }, { label: '📏 Make shorter', action: () => action('make-shorter'), hint: 'Rend le texte plus concis' },
{ label: '📐 Make longer', action: () => action('make-longer') }, { label: '📐 Make longer', action: () => action('make-longer'), hint: 'Ajoute des détails' },
{ label: '📋 Simplify language', action: () => action('simplify') }, { label: '📋 Simplify language', action: () => action('simplify'), hint: 'Simplifie le langage' },
]); ], 'Modifie le texte sélectionné');
// ── Tone menu ── // ── Tone menu ──
const toneBtn = createDropdownBtn('Ton', [ const toneBtn = createDropdownBtn('Ton', [
{ label: '💼 Professional tone', action: () => action('tone', { tone: 'professional' }) }, { label: '💼 Professional tone', action: () => action('tone', { tone: 'professional' }) },
{ label: '💬 Casual tone', action: () => action('tone', { tone: 'casual' }) }, { label: '💬 Casual tone', action: () => action('tone', { tone: 'casual' }) },
]); ], 'Change le ton du texte');
// ── Translate menu ── // ── Translate menu ──
const translateBtn = createDropdownBtn('Traduire', [ const translateBtn = createDropdownBtn('Traduire', [
@ -268,22 +276,23 @@ export async function createAIToolbar(container, getEditorView) {
{ label: '🇩🇪 German', action: () => action('translate', { target_lang: 'German' }) }, { label: '🇩🇪 German', action: () => action('translate', { target_lang: 'German' }) },
{ label: '🇫🇷 French', action: () => action('translate', { target_lang: 'French' }) }, { label: '🇫🇷 French', action: () => action('translate', { target_lang: 'French' }) },
{ label: '🇪🇸 Spanish', action: () => action('translate', { target_lang: 'Spanish' }) }, { label: '🇪🇸 Spanish', action: () => action('translate', { target_lang: 'Spanish' }) },
]); ], 'Traduit le texte sélectionné');
// ── Generate menu ── // ── Generate menu ──
const genBtn = createDropdownBtn('Générer', [ const genBtn = createDropdownBtn('Générer', [
{ label: ' Explain this', action: () => action('explain', {}, 'append') }, { label: ' Explain this', action: () => action('explain', {}, 'append') },
{ label: '📝 Summarize', action: () => action('summarize', {}, 'append') }, { label: '📝 Summarize', action: () => action('summarize', {}, 'append') },
{ label: '✏️ Continue writing', action: () => action('continue', {}, 'append') }, { label: '✏️ Continue writing', action: () => action('continue', {}, 'append') },
]); ], 'Génère du contenu à partir de la sélection');
// ── Custom Rewrite ── // ── Custom Rewrite ──
const rewriteBtn = document.createElement('button'); const rewriteBtn = document.createElement('button');
rewriteBtn.className = 'ai-toolbar-btn'; rewriteBtn.className = 'ai-toolbar-btn';
rewriteBtn.innerHTML = '💬 Réécrire'; rewriteBtn.innerHTML = '💬 Réécrire';
rewriteBtn.title = 'Réécrit le texte selon vos instructions';
Object.assign(rewriteBtn.style, { Object.assign(rewriteBtn.style, {
fontSize: '0.7rem', fontSize: '0.7rem',
background: 'none', background: 'transparent',
border: 'none', border: 'none',
cursor: 'pointer', cursor: 'pointer',
padding: '3px 6px', padding: '3px 6px',
@ -311,7 +320,7 @@ export async function createAIToolbar(container, getEditorView) {
{ label: '📊 Convert to table', action: () => action('to-table') }, { label: '📊 Convert to table', action: () => action('to-table') },
{ label: '⚙️ Generate frontmatter', action: () => action('frontmatter', {}, 'before') }, { label: '⚙️ Generate frontmatter', action: () => action('frontmatter', {}, 'before') },
{ label: '🔷 Convert to canvas', action: () => action('to-canvas', {}, 'append') }, { label: '🔷 Convert to canvas', action: () => action('to-canvas', {}, 'append') },
]); ], 'Outils de conversion');
toolbar.appendChild(aiBtn); toolbar.appendChild(aiBtn);
toolbar.appendChild(createSeparator()); toolbar.appendChild(createSeparator());
@ -363,10 +372,11 @@ export async function createAIToolbar(container, getEditorView) {
return toolbar; return toolbar;
} }
function createDropdownBtn(text, items) { function createDropdownBtn(text, items, tooltip = '') {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = 'ai-toolbar-btn'; btn.className = 'ai-toolbar-btn';
btn.innerHTML = text + ' ▾'; btn.innerHTML = text + ' ▾';
btn.title = tooltip || text;
Object.assign(btn.style, { Object.assign(btn.style, {
position: 'relative', position: 'relative',
fontSize: '0.7rem', fontSize: '0.7rem',

View File

@ -319,7 +319,7 @@ async function openEditor(vaultName, filePath) {
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (update.docChanged) { if (update.docChanged) {
clearTimeout(window._obsigateAutoSaveTimer); clearTimeout(window._obsigateAutoSaveTimer);
window._obsigateAutoSaveTimer = setTimeout(() => saveFile(), 2000); window._obsigateAutoSaveTimer = setTimeout(() => saveFile(true), 2000);
} }
}), }),
]; ];
@ -417,17 +417,19 @@ function closeEditor() {
state.editorPath = null; state.editorPath = null;
} }
async function saveFile() { async function saveFile(silent = false) {
if ((!state.editorView && !state.fallbackEditorEl) || !state.editorVault || !state.editorPath) return; if ((!state.editorView && !state.fallbackEditorEl) || !state.editorVault || !state.editorPath) return;
const content = state.editorView ? state.editorView.state.doc.toString() : state.fallbackEditorEl.value; const content = state.editorView ? state.editorView.state.doc.toString() : state.fallbackEditorEl.value;
const saveBtn = document.getElementById("editor-save"); const saveBtn = document.getElementById("editor-save");
const originalHTML = saveBtn.innerHTML; const originalHTML = saveBtn ? saveBtn.innerHTML : '';
try { try {
if (saveBtn && !silent) {
saveBtn.disabled = true; saveBtn.disabled = true;
saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>'; saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons(); safeCreateIcons();
}
const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}/save?path=${encodeURIComponent(state.editorPath)}`, { const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}/save?path=${encodeURIComponent(state.editorPath)}`, {
method: "PUT", method: "PUT",
@ -440,6 +442,16 @@ async function saveFile() {
throw new Error(error.detail || "Erreur de sauvegarde"); throw new Error(error.detail || "Erreur de sauvegarde");
} }
if (silent) {
// Auto-save: brief visual feedback
if (saveBtn) {
saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>';
safeCreateIcons();
setTimeout(() => {
if (saveBtn) saveBtn.innerHTML = originalHTML;
}, 1500);
}
} else {
saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>'; saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>';
safeCreateIcons(); safeCreateIcons();
@ -451,6 +463,7 @@ async function saveFile() {
openFile(_savedVault, _savedPath); openFile(_savedVault, _savedPath);
} }
}, 800); }, 800);
}
} catch (err) { } catch (err) {
console.error("Save error:", err); console.error("Save error:", err);
alert(`Erreur: ${err.message}`); alert(`Erreur: ${err.message}`);