feat(ai): ajouter une section de la reponse et supporter l'editeur Forge (BUG-057, #102)
L'assistant IA peut desormais inserer sa reponse dans l'editeur Forge (postMessage parent-insert) en plus d'Editer, et chaque bloc de code propose un bouton « Ajouter la section » pour n'inserer que ce bloc.
This commit is contained in:
@@ -1474,6 +1474,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
||||
window.addEventListener('message', function(e) {
|
||||
if (!e.data || !e.data.type) return;
|
||||
if (e.data.type === 'parent-save') { forceSave(); }
|
||||
// #102/BUG-057 — the parent AI assistant's « Ajouter » button inserts its
|
||||
// answer (or a single code block) at the current cursor position.
|
||||
if (e.data.type === 'parent-insert' && typeof e.data.text === 'string') {
|
||||
var p = getPos();
|
||||
insertAtCursor((p.s > 0 ? '\n' : '') + e.data.text);
|
||||
showToast('Texte ajoute au document', 'ok');
|
||||
}
|
||||
// #93 — the parent reloads the document after an external write (AI
|
||||
// assistant edit_file / append_to_file / create_file): re-read from disk
|
||||
// and drop the stale local buffer that would otherwise be autosaved back
|
||||
|
||||
@@ -4319,6 +4319,12 @@
|
||||
Réponses formatées : titres, listes, tableaux, citations et
|
||||
blocs de code.
|
||||
</li>
|
||||
<li data-i18n="help.assistant_insert">
|
||||
Le bouton « Ajouter » (au survol d'une réponse) insère la
|
||||
réponse dans le document ouvert dans l'éditeur (Editer ou
|
||||
Forge) ; chaque bloc de code propose « Ajouter la section »
|
||||
pour n'insérer que ce bloc.
|
||||
</li>
|
||||
<li data-i18n="help.assistant_links">
|
||||
Les fichiers et chemins cités sont des liens : cliquez sur un
|
||||
fichier pour l'ouvrir, sur un dossier pour le révéler dans
|
||||
|
||||
+79
-16
@@ -2153,6 +2153,7 @@ class BooksLM {
|
||||
bubble.className = 'bookslm-bubble assistant';
|
||||
const { text, actions } = this._extractActions(msg.content || '');
|
||||
bubble.innerHTML = this._renderMarkdown(text);
|
||||
this._enhanceCodeBlocks(bubble);
|
||||
// Plain chat has no steps block: the pending answer itself carries the
|
||||
// animated indicator until the first token arrives.
|
||||
if (running && !text && !actions.length) {
|
||||
@@ -2234,28 +2235,90 @@ class BooksLM {
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an assistant answer to the Forge editor document (Notion “Ajouter”).
|
||||
* No-ops with a toast when no editor session is open.
|
||||
* Append an assistant answer (or a single code block) to the document open
|
||||
* in the active editor (Notion “Ajouter”). Supports the three editor
|
||||
* surfaces: CodeMirror (« Editer »), the Forge iframe and the plain textarea
|
||||
* fallback. No-ops with a toast when no editor session is open.
|
||||
*/
|
||||
_insertIntoEditor(text) {
|
||||
const value = String(text == null ? '' : text);
|
||||
if (!value.trim()) return;
|
||||
|
||||
// 1. CodeMirror editor (« Editer »).
|
||||
const view = state.editorView;
|
||||
if (!view || !view.state || typeof view.dispatch !== 'function') {
|
||||
showToast(t('bookslm.insert_no_editor'), 'info');
|
||||
if (view && view.state && typeof view.dispatch === 'function') {
|
||||
try {
|
||||
const at = view.state.selection.main.to;
|
||||
const insert = (at > 0 ? '\n' : '') + value;
|
||||
view.dispatch({
|
||||
changes: { from: at, insert },
|
||||
selection: { anchor: at + insert.length },
|
||||
});
|
||||
if (typeof view.focus === 'function') view.focus();
|
||||
showToast(t('bookslm.inserted'), 'success');
|
||||
} catch (e) {
|
||||
console.warn('AI assistant: insert into editor failed', e);
|
||||
showToast(t('bookslm.insert_no_editor'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const at = view.state.selection.main.to;
|
||||
const insert = (at > 0 ? '\n' : '') + text;
|
||||
view.dispatch({
|
||||
changes: { from: at, insert },
|
||||
selection: { anchor: at + insert.length },
|
||||
});
|
||||
if (typeof view.focus === 'function') view.focus();
|
||||
showToast(t('bookslm.inserted'), 'success');
|
||||
} catch (e) {
|
||||
console.warn('AI assistant: insert into editor failed', e);
|
||||
showToast(t('bookslm.insert_no_editor'), 'error');
|
||||
|
||||
// 2. Forge editor (same-origin iframe): its buffer lives in the child
|
||||
// document, so the insertion is delegated with a postMessage.
|
||||
const forge = document.getElementById('forge-iframe');
|
||||
if (forge && forge.contentWindow) {
|
||||
try {
|
||||
forge.contentWindow.postMessage({ type: 'parent-insert', text: value }, '*');
|
||||
showToast(t('bookslm.inserted'), 'success');
|
||||
} catch (e) {
|
||||
console.warn('AI assistant: insert into Forge failed', e);
|
||||
showToast(t('bookslm.insert_no_editor'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Plain textarea fallback (CodeMirror failed to load).
|
||||
const ta = state.fallbackEditorEl;
|
||||
if (ta && typeof ta.value === 'string') {
|
||||
const start = ta.selectionStart != null ? ta.selectionStart : ta.value.length;
|
||||
const end = ta.selectionEnd != null ? ta.selectionEnd : ta.value.length;
|
||||
const insert = (start > 0 ? '\n' : '') + value;
|
||||
ta.value = ta.value.slice(0, start) + insert + ta.value.slice(end);
|
||||
const pos = start + insert.length;
|
||||
if (typeof ta.setSelectionRange === 'function') ta.setSelectionRange(pos, pos);
|
||||
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
if (typeof ta.focus === 'function') ta.focus();
|
||||
showToast(t('bookslm.inserted'), 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
showToast(t('bookslm.insert_no_editor'), 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* #102 — Add a discreet « Ajouter » button to every fenced code block of an
|
||||
* answer, so a single proposed section can be inserted instead of the whole
|
||||
* reply. The button reuses the message action styling.
|
||||
*/
|
||||
_enhanceCodeBlocks(bubble) {
|
||||
if (!bubble || typeof bubble.querySelectorAll !== 'function') return;
|
||||
const blocks = bubble.querySelectorAll('pre');
|
||||
blocks.forEach((pre) => {
|
||||
const code = pre.querySelector('code');
|
||||
if (!code) return;
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'bookslm-code-block';
|
||||
pre.parentNode.insertBefore(wrap, pre);
|
||||
wrap.appendChild(pre);
|
||||
const btn = this._actionBtn(
|
||||
t('bookslm.insert_block'),
|
||||
'insert',
|
||||
t('bookslm.insert_block_hint'),
|
||||
() => this._insertIntoEditor(code.textContent),
|
||||
);
|
||||
btn.classList.add('bookslm-code-insert');
|
||||
wrap.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
_copyText(text) {
|
||||
|
||||
@@ -1290,6 +1290,7 @@
|
||||
"help.assistant_panel": "🧠 Assistant panel (BooksLM)",
|
||||
"help.assistant_panel_desc": "The side assistant (floating button or a folder's context menu) answers in formatted Markdown and contextualises your directories or documents. In general mode it also knows what you are looking at: open documents, current directory, active search and recently modified files.",
|
||||
"help.assistant_markdown": "Formatted answers: headings, lists, tables, quotes and code blocks.",
|
||||
"help.assistant_insert": "The \"Add\" button (revealed on hover of an answer) inserts the answer into the document open in the editor (Editer or Forge); each code block offers \"Add section\" to insert just that block.",
|
||||
"help.assistant_links": "Cited files and paths are links: a bare filename copies the name to the clipboard, a folder is revealed in the tree, and a file path opens it in the viewer.",
|
||||
"help.assistant_sessions": "The header history icon lists past sessions (reopen or delete); “+” starts a new conversation.",
|
||||
"help.assistant_agent": "The \"agent mode\" button enables tools (read, list, search); modifying actions require confirmation with a change preview.",
|
||||
@@ -1785,6 +1786,8 @@
|
||||
"bookslm.insert_hint": "Append the answer to the document open in the editor",
|
||||
"bookslm.inserted": "Answer added to the document",
|
||||
"bookslm.insert_no_editor": "No document open in the editor",
|
||||
"bookslm.insert_block": "Add section",
|
||||
"bookslm.insert_block_hint": "Add only this code block to the document open in the editor",
|
||||
"ai.steps_count": "{count} step",
|
||||
"ai.steps_count_plural": "{count} steps",
|
||||
"ai.activity_thinking": "Thinking…",
|
||||
|
||||
@@ -1290,6 +1290,7 @@
|
||||
"help.assistant_panel": "🧠 Panneau Assistant (BooksLM)",
|
||||
"help.assistant_panel_desc": "L'assistant latéral (bouton flottant ou menu contextuel d'un dossier) répond en Markdown formaté et contextualise vos répertoires ou documents. En contexte général, il connaît aussi ce que vous voyez : documents ouverts, répertoire courant, recherche en cours et fichiers récemment modifiés.",
|
||||
"help.assistant_markdown": "Réponses formatées : titres, listes, tableaux, citations et blocs de code.",
|
||||
"help.assistant_insert": "Le bouton « Ajouter » (au survol d'une réponse) insère la réponse dans le document ouvert dans l'éditeur (Editer ou Forge) ; chaque bloc de code propose « Ajouter la section » pour n'insérer que ce bloc.",
|
||||
"help.assistant_links": "Les fichiers et chemins cités sont des liens : un simple nom de fichier copie le nom dans le presse-papiers, un dossier est révélé dans l'arborescence, et un chemin de fichier l'ouvre dans le viewer.",
|
||||
"help.assistant_sessions": "L'icône historique de l'en-tête liste les sessions passées (recharger ou supprimer) ; « + » démarre une nouvelle conversation.",
|
||||
"help.assistant_agent": "Le bouton « mode agent » active les outils (lire, lister, chercher) ; les actions de modification demandent une confirmation avec aperçu des changements.",
|
||||
@@ -1785,6 +1786,8 @@
|
||||
"bookslm.insert_hint": "Ajouter la réponse au document ouvert dans l'éditeur",
|
||||
"bookslm.inserted": "Réponse ajoutée au document",
|
||||
"bookslm.insert_no_editor": "Aucun document ouvert dans l'éditeur",
|
||||
"bookslm.insert_block": "Ajouter la section",
|
||||
"bookslm.insert_block_hint": "Ajouter uniquement ce bloc de code au document ouvert dans l'éditeur",
|
||||
"ai.steps_count": "{count} étape",
|
||||
"ai.steps_count_plural": "{count} étapes",
|
||||
"ai.activity_thinking": "Réflexion…",
|
||||
|
||||
@@ -9531,6 +9531,14 @@ body.popup-mode .content-area {
|
||||
.bookslm-bubble.assistant { align-self: flex-start; background: transparent; padding: 0; width: 100%; max-width: 100%; color: var(--text-primary); }
|
||||
.bookslm-bubble.assistant code { background: rgba(0,0,0,0.2); padding: 1px 4px; border-radius: 3px; font-size: 0.9em; }
|
||||
.bookslm-bubble.assistant pre { background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; overflow-x: auto; margin: 8px 0; }
|
||||
/* #102 — per-code-block “Ajouter”: a discreet button in the block's corner,
|
||||
revealed on hover/focus, that inserts only this section. */
|
||||
.bookslm-code-block { position: relative; }
|
||||
.bookslm-code-block .bookslm-code-insert { position: absolute; top: 6px; right: 6px;
|
||||
z-index: 1; background: var(--surface2); border-color: var(--border); opacity: 0;
|
||||
transition: opacity 0.15s; }
|
||||
.bookslm-code-block:hover .bookslm-code-insert,
|
||||
.bookslm-code-block:focus-within .bookslm-code-insert { opacity: 1; }
|
||||
.bookslm-sources { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
|
||||
.bookslm-source-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px;
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||||
|
||||
Reference in New Issue
Block a user