diff --git a/frontend/popout.html b/frontend/popout.html
index d23651b..f83dad9 100644
--- a/frontend/popout.html
+++ b/frontend/popout.html
@@ -927,6 +927,11 @@ const RightSidebarManager = {
cachedRawSource = rawData.raw;
bodyEl.innerHTML = "";
+ // Create AI toolbar container (before CodeMirror)
+ const aiContainer = document.createElement('div');
+ aiContainer.id = 'ai-toolbar-container';
+ bodyEl.appendChild(aiContainer);
+
if (editorView) {
editorView.destroy();
editorView = null;
@@ -1008,6 +1013,12 @@ const RightSidebarManager = {
}
modal.classList.add("active");
+ // Initialize AI toolbar (after editor is ready)
+ if (window._popoutAIToolbarCreated) {
+ // Update the existing toolbar's editor reference
+ window._popoutAIToolbarCreated = false;
+ }
+ createPopoutAIToolbar(aiContainer, () => editorView);
safeCreateIcons();
}
@@ -1021,6 +1032,278 @@ const RightSidebarManager = {
fallbackEditorEl = null;
editorVault = null;
editorPath = null;
+ // Clean up AI toolbar
+ const aiContainer = document.getElementById('ai-toolbar-container');
+ if (aiContainer) aiContainer.innerHTML = '';
+ }
+
+ // ── AI Toolbar (inline, same as main app's ai.js) ──
+
+ async function popoutFetch(url, opts = {}) {
+ const res = await fetch(url, { ...opts, credentials: 'include' });
+ if (!res.ok) {
+ let detail = '';
+ try { const err = await res.json(); detail = err.detail || ''; } catch {}
+ throw new Error(detail || `HTTP ${res.status}`);
+ }
+ return res.json();
+ }
+
+ async function popoutAiAction(endpoint, text, extra = {}) {
+ const body = { text, ...extra };
+ const data = await popoutFetch(`/api/ai/${endpoint}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+ return data.result;
+ }
+
+ function popoutGetSelection(ev) {
+ if (!ev) return '';
+ const { from, to } = ev.state.selection.main;
+ if (from === to) return ev.state.doc.toString();
+ return ev.state.doc.sliceString(from, to);
+ }
+
+ function popoutReplaceSelection(ev, newText, mode = 'replace') {
+ if (!ev) return;
+ const { from, to } = ev.state.selection.main;
+ if (mode === 'append') {
+ ev.dispatch({ changes: { from: to, insert: '\n' + newText }, selection: { anchor: to + 1 + newText.length } });
+ } else if (mode === 'before') {
+ ev.dispatch({ changes: { from, insert: newText + '\n' } });
+ } else {
+ ev.dispatch({ changes: { from, to, insert: newText }, selection: { anchor: from + newText.length } });
+ }
+ }
+
+ function popoutShowToast(msg, type) {
+ const existing = document.querySelector('.popout-toast');
+ if (existing) existing.remove();
+ const toast = document.createElement('div');
+ toast.className = 'popout-toast';
+ const colors = { success: '#22c55e', error: '#ef4444', info: '#3b82f6', warning: '#f59e0b' };
+ Object.assign(toast.style, {
+ position: 'fixed', bottom: '16px', right: '16px', zIndex: '9999',
+ background: 'var(--bg-secondary)', color: 'var(--text-primary)',
+ padding: '8px 16px', borderRadius: '6px', fontSize: '0.8rem',
+ borderLeft: `3px solid ${colors[type] || colors.info}`,
+ boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
+ transition: 'opacity 0.3s', opacity: '1',
+ });
+ toast.textContent = msg;
+ document.body.appendChild(toast);
+ setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 2500);
+ }
+
+ function popoutCreateMenu(items, parentEl) {
+ const existing = parentEl.querySelector('.ai-dropdown-menu');
+ if (existing) existing.remove();
+ const menu = document.createElement('div');
+ menu.className = 'ai-dropdown-menu';
+ Object.assign(menu.style, {
+ position: 'absolute', top: '100%', left: '0',
+ background: 'var(--bg-primary)', border: '1px solid var(--border-color)',
+ borderRadius: '6px', padding: '4px 0', minWidth: '200px', zIndex: '100',
+ boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
+ });
+ items.forEach(item => {
+ const el = document.createElement('div');
+ el.className = 'ai-menu-item';
+ Object.assign(el.style, {
+ padding: '6px 12px', cursor: 'pointer', display: 'flex', alignItems: 'center',
+ gap: '8px', fontSize: '0.8rem', color: 'var(--text-primary)',
+ whiteSpace: 'nowrap', background: 'transparent',
+ });
+ el.innerHTML = item.label;
+ if (item.hint) {
+ const hint = document.createElement('span');
+ hint.style.cssText = 'margin-left:auto;font-size:0.65rem;color:var(--text-muted)';
+ hint.textContent = item.hint;
+ el.appendChild(hint);
+ }
+ el.addEventListener('mouseenter', () => { el.style.background = 'rgba(255,255,255,0.06)'; });
+ el.addEventListener('mouseleave', () => { el.style.background = 'transparent'; });
+ el.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if (item.action) item.action();
+ menu.remove();
+ });
+ menu.appendChild(el);
+ });
+ parentEl.appendChild(menu);
+ const closeHandler = (e) => {
+ if (!menu.contains(e.target)) { menu.remove(); document.removeEventListener('click', closeHandler); }
+ };
+ setTimeout(() => document.addEventListener('click', closeHandler), 0);
+ }
+
+ function popoutCreateDropdownBtn(text, items, tooltip) {
+ const btn = document.createElement('button');
+ btn.className = 'ai-toolbar-btn';
+ btn.innerHTML = text + ' ▾';
+ btn.title = tooltip || text;
+ Object.assign(btn.style, {
+ position: 'relative', fontSize: '0.7rem', background: 'none',
+ border: 'none', cursor: 'pointer', padding: '3px 6px',
+ borderRadius: '4px', color: 'var(--text-primary)',
+ });
+ btn.addEventListener('click', (e) => { e.stopPropagation(); popoutCreateMenu(items, btn); });
+ btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.08)'; });
+ btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
+ return btn;
+ }
+
+ async function createPopoutAIToolbar(container, getEditorView) {
+ if (window._popoutAIToolbarCreated) return;
+ window._popoutAIToolbarCreated = true;
+
+ // Check if AI is configured
+ let aiConfigured = false;
+ try {
+ const status = await popoutFetch('/api/ai/status');
+ aiConfigured = status.configured;
+ } catch { aiConfigured = false; }
+
+ if (!aiConfigured) {
+ const hint = document.createElement('div');
+ hint.style.cssText = 'padding:6px 12px;font-size:0.7rem;color:var(--text-muted);border-bottom:1px solid var(--border-color)';
+ hint.textContent = 'AI non configuré — ajouter une clé API dans .env';
+ container.appendChild(hint);
+ return;
+ }
+
+ function ev() { return getEditorView(); }
+
+ const toolbar = document.createElement('div');
+ toolbar.className = 'ai-toolbar';
+ Object.assign(toolbar.style, {
+ display: 'flex', alignItems: 'center', gap: '2px', padding: '4px 8px',
+ borderBottom: '1px solid var(--border-color)', background: '#2a2a2a', flexWrap: 'wrap',
+ });
+
+ // AI inline complete button
+ const aiBtn = document.createElement('button');
+ aiBtn.className = 'ai-toolbar-btn';
+ aiBtn.innerHTML = '✦ AI';
+ Object.assign(aiBtn.style, {
+ color: '#60a5fa', fontWeight: '600', fontSize: '0.75rem',
+ background: 'none', border: 'none', cursor: 'pointer',
+ padding: '3px 8px', borderRadius: '4px',
+ });
+ aiBtn.title = 'AI Inline Completion (Ctrl+J)';
+ aiBtn.addEventListener('click', async () => {
+ const v = ev(); if (!v) return;
+ const text = popoutGetSelection(v);
+ if (!text.trim()) return;
+ const origHTML = aiBtn.innerHTML;
+ aiBtn.innerHTML = '⏳ AI...'; aiBtn.disabled = true;
+ try {
+ const result = await popoutAiAction('inline-complete', text);
+ popoutReplaceSelection(v, result, 'append');
+ popoutShowToast('AI: complétion ajoutée', 'success');
+ } catch (e) {
+ popoutShowToast('AI: ' + (String(e.message).includes('401') ? 'clé API invalide' : e.message), 'error');
+ } finally {
+ aiBtn.innerHTML = origHTML; aiBtn.disabled = false;
+ }
+ });
+
+ async function popoutAction(endpoint, extra = {}, mode = 'replace') {
+ const v = ev(); if (!v) return;
+ const text = popoutGetSelection(v);
+ if (!text.trim()) { popoutShowToast('AI: sélectionnez du texte', 'warning'); return; }
+ popoutShowToast('⏳ AI: traitement...', 'info');
+ try {
+ const result = await popoutAiAction(endpoint, text, extra);
+ popoutReplaceSelection(v, result, mode);
+ popoutShowToast('AI: ✓', 'success');
+ } catch (e) {
+ const msg = String(e.message);
+ if (msg.includes('401')) popoutShowToast('AI: clé API invalide', 'error');
+ else if (msg.includes('402') || msg.includes('429')) popoutShowToast('AI: quota dépassé', 'error');
+ else popoutShowToast('AI: ' + msg, 'error');
+ }
+ }
+
+ const sep = (() => { const s = document.createElement('span'); s.style.cssText = 'width:1px;height:16px;background:var(--border-color);margin:0 2px'; return s; });
+
+ toolbar.appendChild(aiBtn);
+ toolbar.appendChild(sep());
+
+ toolbar.appendChild(popoutCreateDropdownBtn('Éditer', [
+ { label: '🪄 Improve writing', action: () => popoutAction('improve') },
+ { label: '🔤 Fix spelling & grammar', action: () => popoutAction('fix-spelling') },
+ { label: '📏 Make shorter', action: () => popoutAction('make-shorter') },
+ { label: '📐 Make longer', action: () => popoutAction('make-longer') },
+ { label: '📋 Simplify language', action: () => popoutAction('simplify') },
+ ], 'Modifie le texte sélectionné'));
+
+ toolbar.appendChild(popoutCreateDropdownBtn('Ton', [
+ { label: '💼 Professional tone', action: () => popoutAction('tone', { tone: 'professional' }) },
+ { label: '💬 Casual tone', action: () => popoutAction('tone', { tone: 'casual' }) },
+ ], 'Change le ton du texte'));
+
+ toolbar.appendChild(popoutCreateDropdownBtn('Traduire', [
+ { label: '🇬🇧 English', action: () => popoutAction('translate', { target_lang: 'English' }) },
+ { label: '🇨🇳 Chinese', action: () => popoutAction('translate', { target_lang: 'Chinese' }) },
+ { label: '🇯🇵 Japanese', action: () => popoutAction('translate', { target_lang: 'Japanese' }) },
+ { label: '🇩🇪 German', action: () => popoutAction('translate', { target_lang: 'German' }) },
+ { label: '🇫🇷 French', action: () => popoutAction('translate', { target_lang: 'French' }) },
+ { label: '🇪🇸 Spanish', action: () => popoutAction('translate', { target_lang: 'Spanish' }) },
+ ], 'Traduit le texte sélectionné'));
+
+ toolbar.appendChild(sep());
+
+ toolbar.appendChild(popoutCreateDropdownBtn('Générer', [
+ { label: 'ℹ️ Explain this', action: () => popoutAction('explain', {}, 'append') },
+ { label: '📝 Summarize', action: () => popoutAction('summarize', {}, 'append') },
+ { label: '✏️ Continue writing', action: () => popoutAction('continue', {}, 'append') },
+ ], 'Génère du contenu'));
+
+ const rewriteBtn = document.createElement('button');
+ rewriteBtn.className = 'ai-toolbar-btn';
+ rewriteBtn.innerHTML = '💬 Réécrire';
+ rewriteBtn.title = 'Réécrit selon vos instructions';
+ Object.assign(rewriteBtn.style, {
+ fontSize: '0.7rem', background: 'transparent', border: 'none',
+ cursor: 'pointer', padding: '3px 6px', borderRadius: '4px', color: 'var(--text-primary)',
+ });
+ rewriteBtn.addEventListener('click', async () => {
+ const v = ev(); if (!v) return;
+ const text = popoutGetSelection(v);
+ const instruction = prompt('Instruction de réécriture :', '');
+ if (!instruction) return;
+ try {
+ const result = await popoutAiAction('rewrite', text, { instruction });
+ popoutReplaceSelection(v, result);
+ popoutShowToast('AI: texte réécrit', 'success');
+ } catch (e) {
+ popoutShowToast('AI: ' + (String(e.message).includes('401') ? 'clé API invalide' : e.message), 'error');
+ }
+ });
+ toolbar.appendChild(rewriteBtn);
+
+ toolbar.appendChild(popoutCreateDropdownBtn('🧰 Boîte', [
+ { label: '📋 Convert to list', action: () => popoutAction('to-list') },
+ { label: '📊 Convert to table', action: () => popoutAction('to-table') },
+ { label: '⚙️ Generate frontmatter', action: () => popoutAction('frontmatter', {}, 'before') },
+ { label: '🔷 Convert to canvas', action: () => popoutAction('to-canvas', {}, 'append') },
+ ], 'Outils de conversion'));
+
+ container.innerHTML = ''; // clear previous
+ container.appendChild(toolbar);
+
+ // Ctrl+J keyboard shortcut
+ document.addEventListener('keydown', function popoutAIKey(e) {
+ if (e.ctrlKey && e.key === 'j') {
+ const modal = document.getElementById('editor-modal');
+ if (!modal || !modal.classList.contains('active')) return;
+ e.preventDefault();
+ aiBtn.click();
+ }
+ });
}
async function saveFile() {