|
|
|
@@ -100,27 +100,6 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
.preview-content img { max-width: 100%; border-radius: var(--r); }
|
|
|
|
|
.preview-loading { text-align: center; padding: 40px; color: var(--text3); font-size: 13px; }
|
|
|
|
|
|
|
|
|
|
/* AI Panel (right) */
|
|
|
|
|
.ai-panel { width: 320px; min-width: 320px; background: var(--bg2); border-left: 1px solid var(--border); display: flex; flex-direction: column; transition: transform 200ms, min-width 200ms, width 200ms; overflow: hidden; }
|
|
|
|
|
.ai-panel.hidden { transform: translateX(100%); min-width: 0; width: 0; border-left: none; }
|
|
|
|
|
.ai-panel-head { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
|
|
|
|
|
.ai-panel-title { font-weight: 700; font-size: 13px; color: var(--ai); }
|
|
|
|
|
.ai-panel-close { width: 26px; height: 26px; border-radius: var(--r); border: none; background: transparent; color: var(--text3); cursor: pointer; font-size: 15px; }
|
|
|
|
|
.ai-panel-close:hover { background: var(--bg-hover); color: var(--text); }
|
|
|
|
|
.ai-chat { flex: 1; padding: 14px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
|
|
|
|
|
.ai-suggestions { padding: 10px 14px; border-top: 1px solid var(--border); }
|
|
|
|
|
.ai-sugg { display: block; width: 100%; text-align: left; padding: 7px 10px; margin-bottom: 3px; border-radius: var(--r); border: none; background: transparent; color: var(--text2); cursor: pointer; font-size: 12px; font-family: var(--sans); }
|
|
|
|
|
.ai-sugg:hover { background: var(--bg-hover); color: var(--text); }
|
|
|
|
|
.ai-input-row { padding: 8px 14px; border-top: 1px solid var(--border); display: flex; gap: 6px; }
|
|
|
|
|
.ai-input { flex: 1; padding: 7px 10px; border-radius: var(--r); border: 1px solid var(--border); background: var(--bg3); color: var(--text); font-size: 12px; outline: none; font-family: var(--sans); }
|
|
|
|
|
.ai-input:focus { border-color: var(--ai); }
|
|
|
|
|
.ai-send { padding: 7px 12px; border-radius: var(--r); border: none; background: var(--ai); color: #fff; cursor: pointer; font-size: 12px; font-weight: 600; }
|
|
|
|
|
.ai-msg { padding: 8px 11px; border-radius: var(--r); font-size: 12px; line-height: 1.5; max-width: 90%; white-space: pre-wrap; }
|
|
|
|
|
.ai-msg.user { background: var(--bg3); align-self: flex-end; }
|
|
|
|
|
.ai-msg.bot { background: var(--ai-bg); color: var(--ai); align-self: flex-start; }
|
|
|
|
|
.ai-msg.loading { color: var(--text3); font-style: italic; align-self: flex-start; }
|
|
|
|
|
.ai-chat-placeholder { color: var(--text3); font-size: 12px; padding: 10px; }
|
|
|
|
|
|
|
|
|
|
/* Slash menu */
|
|
|
|
|
.slash { position: fixed; z-index: 200; width: 300px; max-height: 360px; overflow-y: auto; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; }
|
|
|
|
|
.slash.on { display: block; animation: fadeIn 120ms; }
|
|
|
|
@@ -264,7 +243,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
<div class="bar-right">
|
|
|
|
|
<button class="btn" id="btn-help" title="Aide / Raccourcis (F1)">?</button>
|
|
|
|
|
<button class="btn" id="btn-preview" title="Toggle preview">👁</button>
|
|
|
|
|
<button class="btn btn-ai off" id="btn-ai" title="AI Panel (Ctrl+J)">✨</button>
|
|
|
|
|
<button class="btn btn-ai off" id="btn-ai" title="Assistant IA (Ctrl+J)">✨</button>
|
|
|
|
|
<button class="btn" id="btn-fullscreen" title="Plein écran (F11)" aria-label="Plein écran"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg></button>
|
|
|
|
|
<div class="save-dot ok" id="save-dot"><span class="dot"></span><span id="save-label">Saved</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
@@ -380,33 +360,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
<div class="bubble-more-item" data-action="ai-continue">➕ Continue writing</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- AI Panel -->
|
|
|
|
|
<div class="ai-panel hidden" id="ai-panel">
|
|
|
|
|
<div class="ai-panel-head">
|
|
|
|
|
<span class="ai-panel-title">✨ AI Assistant</span>
|
|
|
|
|
<button class="ai-panel-close" id="ai-close">×</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-chat" id="ai-chat">
|
|
|
|
|
<div class="ai-chat-placeholder">Ask AI about your document. Use slash /ai or select text for AI actions.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-suggestions">
|
|
|
|
|
<button class="ai-sugg">✨ Improve overall style</button>
|
|
|
|
|
<button class="ai-sugg">➕ Add a conclusion</button>
|
|
|
|
|
<button class="ai-sugg">📝 Summarize document</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-input-row">
|
|
|
|
|
<input class="ai-input" id="ai-input" placeholder="Ask AI...">
|
|
|
|
|
<button class="ai-send" id="ai-send">Send</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Status bar -->
|
|
|
|
|
<div class="stat">
|
|
|
|
|
<div class="stat-left">
|
|
|
|
|
<span>Type <strong>/</strong> for commands | <strong>Alt+\</strong> autocomplete | <strong>Alt+I</strong> Insert</span>
|
|
|
|
|
<span>Ctrl+J AI panel</span>
|
|
|
|
|
<span>Ctrl+J Assistant IA</span>
|
|
|
|
|
<span>Ctrl+S save</span>
|
|
|
|
|
<span>Ctrl+K link</span>
|
|
|
|
|
<span class="stat-ai" id="stat-ai">✨ AI processing...</span>
|
|
|
|
@@ -453,7 +413,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
<table>
|
|
|
|
|
<tr><td>Ctrl+S</td><td>Sauvegarder</td></tr>
|
|
|
|
|
<tr><td>Clic sur le titre</td><td>Renommer le fichier</td></tr>
|
|
|
|
|
<tr><td>Ctrl+J</td><td>Panneau AI</td></tr>
|
|
|
|
|
<tr><td>Ctrl+J</td><td>Assistant IA</td></tr>
|
|
|
|
|
<tr><td>Alt+\</td><td>Autocompletion intelligente</td></tr>
|
|
|
|
|
<tr><td>Enter (liste)</td><td>Continue la liste (-, *, 1.) et checkboxes</td></tr>
|
|
|
|
|
<tr><td>Enter (liste vide)</td><td>Termine la liste</td></tr>
|
|
|
|
@@ -494,7 +454,6 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
var bubble = document.getElementById('bubble');
|
|
|
|
|
var bubbleMore = document.getElementById('bubble-more-menu');
|
|
|
|
|
var qiMenu = document.getElementById('qi-menu');
|
|
|
|
|
var aiPanel = document.getElementById('ai-panel');
|
|
|
|
|
var btnAI = document.getElementById('btn-ai');
|
|
|
|
|
var btnPreview = document.getElementById('btn-preview');
|
|
|
|
|
var saveDot = document.getElementById('save-dot');
|
|
|
|
@@ -699,6 +658,21 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- AI calls ----
|
|
|
|
|
// #101 — Use the provider/model selected in the AI Assistant. The picker
|
|
|
|
|
// stores its choice in the same-origin `localStorage` key shared with the
|
|
|
|
|
// parent app (`obsigate_ai_picker`), so Forge AI actions follow the assistant.
|
|
|
|
|
function aiPickerSelection() {
|
|
|
|
|
try {
|
|
|
|
|
var raw = localStorage.getItem('obsigate_ai_picker');
|
|
|
|
|
if (!raw) return {};
|
|
|
|
|
var p = JSON.parse(raw);
|
|
|
|
|
var out = {};
|
|
|
|
|
if (p && p.provider) out.provider = p.provider;
|
|
|
|
|
if (p && p.model) out.model = p.model;
|
|
|
|
|
return out;
|
|
|
|
|
} catch (e) { return {}; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function aiCall(endpoint, text, extra, callback) {
|
|
|
|
|
if (!aiConfigured) {
|
|
|
|
|
// Check status first, then retry
|
|
|
|
@@ -712,6 +686,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
}
|
|
|
|
|
showAIProcessing();
|
|
|
|
|
var body = { text: text };
|
|
|
|
|
var pick = aiPickerSelection();
|
|
|
|
|
Object.keys(pick).forEach(function(k) { body[k] = pick[k]; });
|
|
|
|
|
if (extra) Object.keys(extra).forEach(function(k) { body[k] = extra[k]; });
|
|
|
|
|
fetch('/api/ai/' + endpoint, {
|
|
|
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
|
|
@@ -739,10 +715,10 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
|
|
|
|
|
function updateAIButton() {
|
|
|
|
|
if (aiConfigured) {
|
|
|
|
|
btnAI.className = 'btn btn-ai'; btnAI.title = 'AI Panel (Ctrl+J)';
|
|
|
|
|
btnAI.className = 'btn btn-ai'; btnAI.title = 'Assistant IA (Ctrl+J)';
|
|
|
|
|
document.getElementById('stat-ai-ready').style.display = 'inline';
|
|
|
|
|
} else {
|
|
|
|
|
btnAI.className = 'btn btn-ai off'; btnAI.title = 'AI not configured';
|
|
|
|
|
btnAI.className = 'btn btn-ai off'; btnAI.title = 'Assistant IA non configure';
|
|
|
|
|
document.getElementById('stat-ai-ready').style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -756,9 +732,9 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
'ai-fix': { ep: 'fix-spelling', desc: 'fix spelling' },
|
|
|
|
|
'ai-continue': { ep: 'continue', desc: 'continue writing' },
|
|
|
|
|
'ai-summarize': { ep: 'summarize', desc: 'summarize' },
|
|
|
|
|
'ai-translate': { ep: 'translate', extra: { language: 'en' }, desc: 'translate' },
|
|
|
|
|
'ai-shorter': { ep: 'simplify', desc: 'make shorter' },
|
|
|
|
|
'ai-longer': { ep: 'lengthen', desc: 'make longer' },
|
|
|
|
|
'ai-translate': { ep: 'translate', extra: { target_lang: 'en' }, desc: 'translate' },
|
|
|
|
|
'ai-shorter': { ep: 'make-shorter', desc: 'make shorter' },
|
|
|
|
|
'ai-longer': { ep: 'make-longer', desc: 'make longer' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function execAIAction(actionKey) {
|
|
|
|
@@ -1173,36 +1149,38 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
showQI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- AI Panel ----
|
|
|
|
|
function toggleAIPanel() {
|
|
|
|
|
aiPanel.classList.toggle('hidden');
|
|
|
|
|
btnAI.classList.toggle('active', !aiPanel.classList.contains('hidden'));
|
|
|
|
|
// ---- AI Assistant (#101) ----
|
|
|
|
|
// The rich assistant (provider/model picker, streaming, `/` skills, `@`
|
|
|
|
|
// context, history) lives in the parent application. Forge is a same-origin
|
|
|
|
|
// iframe, so the AI button simply asks the parent to open it — no duplicated
|
|
|
|
|
// panel, and the configured provider/model is shared automatically.
|
|
|
|
|
function openAssistant() {
|
|
|
|
|
try {
|
|
|
|
|
window.parent.postMessage({ type: 'forge-open-ai' }, '*');
|
|
|
|
|
} catch (e) { /* not embedded */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sendAIChat() {
|
|
|
|
|
var input = document.getElementById('ai-input');
|
|
|
|
|
var chat = document.getElementById('ai-chat');
|
|
|
|
|
var text = input.value.trim();
|
|
|
|
|
if (!text) return;
|
|
|
|
|
input.value = '';
|
|
|
|
|
// ---- Fullscreen (#101) ----
|
|
|
|
|
var btnFullscreen = document.getElementById('btn-fullscreen');
|
|
|
|
|
var FS_ENTER = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
|
|
|
|
|
var FS_EXIT = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>';
|
|
|
|
|
|
|
|
|
|
var um = document.createElement('div'); um.className = 'ai-msg user'; um.textContent = text;
|
|
|
|
|
chat.appendChild(um);
|
|
|
|
|
|
|
|
|
|
var lm = document.createElement('div'); lm.className = 'ai-msg loading';
|
|
|
|
|
lm.innerHTML = '<span class="spin"></span>Thinking...';
|
|
|
|
|
chat.appendChild(lm);
|
|
|
|
|
chat.scrollTop = chat.scrollHeight;
|
|
|
|
|
|
|
|
|
|
aiCall('improve', text, null, function(result) {
|
|
|
|
|
lm.remove();
|
|
|
|
|
var bm = document.createElement('div'); bm.className = 'ai-msg bot';
|
|
|
|
|
bm.textContent = result || '(no response)';
|
|
|
|
|
chat.appendChild(bm);
|
|
|
|
|
chat.scrollTop = chat.scrollHeight;
|
|
|
|
|
});
|
|
|
|
|
function toggleFullscreen() {
|
|
|
|
|
if (!document.fullscreenElement) {
|
|
|
|
|
var req = document.documentElement.requestFullscreen && document.documentElement.requestFullscreen();
|
|
|
|
|
if (req && req.catch) req.catch(function() { showToast('Plein écran indisponible', 'err'); });
|
|
|
|
|
} else if (document.exitFullscreen) {
|
|
|
|
|
document.exitFullscreen();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('fullscreenchange', function() {
|
|
|
|
|
var on = !!document.fullscreenElement;
|
|
|
|
|
btnFullscreen.innerHTML = on ? FS_EXIT : FS_ENTER;
|
|
|
|
|
btnFullscreen.title = on ? 'Quitter le plein écran' : 'Plein écran (F11)';
|
|
|
|
|
btnFullscreen.classList.toggle('active', on);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- Drag & Drop ----
|
|
|
|
|
var dragC = 0;
|
|
|
|
|
document.addEventListener('dragenter', function(e) { e.preventDefault(); dragC++; if (dragC === 1) dropOverlay.classList.add('on'); });
|
|
|
|
@@ -1292,7 +1270,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
var before = l.text.substring(0, p.s - l.start);
|
|
|
|
|
if (before === '' || /^\s*$/.test(before)) setTimeout(showSlash, 20);
|
|
|
|
|
}
|
|
|
|
|
if (e.ctrlKey && e.key === 'j') { e.preventDefault(); toggleAIPanel(); }
|
|
|
|
|
if (e.ctrlKey && e.key === 'j') { e.preventDefault(); openAssistant(); }
|
|
|
|
|
if (e.ctrlKey && e.key === 's') { e.preventDefault(); forceSave(); }
|
|
|
|
|
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
|
|
|
|
|
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); wrapSelection('*'); }
|
|
|
|
@@ -1433,16 +1411,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
if (langPicker.classList.contains('on') && !langPicker.contains(e.target)) { langPicker.classList.remove('on'); ta.focus(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
btnAI.addEventListener('click', toggleAIPanel);
|
|
|
|
|
document.getElementById('ai-close').addEventListener('click', toggleAIPanel);
|
|
|
|
|
document.getElementById('ai-send').addEventListener('click', sendAIChat);
|
|
|
|
|
document.getElementById('ai-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') sendAIChat(); });
|
|
|
|
|
document.querySelectorAll('.ai-sugg').forEach(function(b) {
|
|
|
|
|
b.addEventListener('click', function() {
|
|
|
|
|
document.getElementById('ai-input').value = b.textContent.trim();
|
|
|
|
|
sendAIChat();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
btnAI.addEventListener('click', openAssistant);
|
|
|
|
|
btnFullscreen.addEventListener('click', toggleFullscreen);
|
|
|
|
|
|
|
|
|
|
btnPreview.addEventListener('click', togglePreview);
|
|
|
|
|
|
|
|
|
@@ -1852,10 +1822,17 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
prompt = 'Continue this ' + langName + ' text naturally. Return ONLY the new text (do NOT repeat):\n' + fullText;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var ghostBody = { text: prompt };
|
|
|
|
|
// #101 — follow the AI Assistant's configured provider/model; fall back to
|
|
|
|
|
// the local Ollama model when the assistant has no explicit selection.
|
|
|
|
|
var ghostPick = aiPickerSelection();
|
|
|
|
|
ghostBody.provider = ghostPick.provider || 'ollama';
|
|
|
|
|
if (ghostPick.model) ghostBody.model = ghostPick.model;
|
|
|
|
|
|
|
|
|
|
fetch('/api/ai/inline-complete', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ text: prompt, provider: 'ollama' })
|
|
|
|
|
body: JSON.stringify(ghostBody)
|
|
|
|
|
})
|
|
|
|
|
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
|
|
|
.then(function(data) {
|
|
|
|
@@ -2000,7 +1977,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|
|
|
|
console.log('ObsiGate Editor v2 ready');
|
|
|
|
|
console.log(' File: ' + (fileVault ? fileVault + '/' + filePath : 'demo mode'));
|
|
|
|
|
console.log(' AI: checking...');
|
|
|
|
|
console.log(' Type / for commands | Select text for bubble | Alt+\\ autocomplete | Ctrl+J AI | Ctrl+S save');
|
|
|
|
|
console.log(' Type / for commands | Select text for bubble | Alt+\\ autocomplete | Ctrl+J Assistant IA | Ctrl+S save');
|
|
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|