Add AI toolbar to popout HTML editor
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user