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:
+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) {
|
||||
|
||||
Reference in New Issue
Block a user