feat(editeur): #93 edition inline — Editer/Forge remplacent la vue lecture
Le conteneur d'edition (#editor-container, CodeMirror ou iframe Forge) est deplace dans la zone de contenu du document (#content-area ou pane active) au lieu de l'overlay plein ecran : le mode edition remplace la vue lecture. L'overlay reste monte (transparent, pointer-events: none) car le ruban d'edition mobile y est ancre ; il ne sert plus que de repli quand la cible n'est pas le document affiche. Garde-fou dans renderFile() pour liberer proprement la session (destroy CodeMirror/Yjs, retrait de l'iframe Forge) ; forge-close passe par closeEditor() ; cache d'onglet invalide au retour en lecture. Assistant IA : app_context.editing annonce le document en cours d'edition (frontend bookslm.js + backend bookslm.py), et chaque ecriture d'outil (edit_file, append_to_file, create_file, restore_backup) recharge le document affiche (obsigate:file-written) — tampon CodeMirror remplace avec auto-save neutralisee, parent-reload pour l'iframe Forge, re-rendu de la vue lecture sinon. Tests : tests/frontend/editor-inline.test.mjs (19), tests/test_bookslm.py::TestGeneralPrompt (3 nouveaux). Fiche : docs/features/editeur-inline.md.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# #93 — Édition inline : « Editer » et « Forge » remplacent la vue lecture
|
||||
|
||||
> **Statut :** 🟢 livré (en attente de vérification utilisateur)
|
||||
> **Version :** 2.3.0
|
||||
> **Composants :** `frontend/js/editor-inline.js` (nouveau), `frontend/js/utils.js`,
|
||||
> `frontend/js/viewer.js`, `frontend/js/sync.js`, `frontend/js/bookslm.js`,
|
||||
> `frontend/editor-poc.html`, `frontend/index.html`, `frontend/style.css`,
|
||||
> `backend/bookslm.py`
|
||||
> **Tests :** `tests/frontend/editor-inline.test.mjs` (19),
|
||||
> `tests/test_bookslm.py::TestGeneralPrompt` (7)
|
||||
|
||||
## Contexte
|
||||
|
||||
Le mode édition (bouton **Editer**, CodeMirror) et **Forge** (éditeur avancé dans une iframe)
|
||||
s'ouvraient dans `#editor-modal` : un overlay `position: fixed` plein écran. Le document restait
|
||||
rendu **dessous**, mais invisible, et l'overlay recouvrait le panneau de l'assistant IA
|
||||
(`.bookslm-panel`, `z-index: 100` contre `1000` pour la modale). Conséquences :
|
||||
|
||||
- le document n'était plus consultable pendant l'édition (pas de relecture côte à côte) ;
|
||||
- l'assistant IA était inutilisable pendant l'édition : son action « Ajouter » (qui insère dans
|
||||
`state.editorView`) exigeait une session d'édition ouverte… que l'utilisateur ne pouvait pas
|
||||
regarder en même temps que l'assistant.
|
||||
|
||||
## Conception
|
||||
|
||||
### 1. Bascule du conteneur d'édition dans la zone de lecture
|
||||
|
||||
`frontend/js/editor-inline.js` (module autonome, sans dépendance) déplace le conteneur
|
||||
`#editor-container` — qui contient le CodeMirror **ou** l'iframe Forge — de l'overlay vers la
|
||||
zone de contenu :
|
||||
|
||||
```
|
||||
#content-area (ou .pane-content de la pane active) ← le document en lecture
|
||||
└── .editor-container.editor-inline ← l'éditeur, à la place du document
|
||||
```
|
||||
|
||||
| Fonction | Rôle |
|
||||
|---|---|
|
||||
| `getEditorContainer()` / `getEditorModal()` | résolution par id (`#editor-container`, `#editor-modal`) |
|
||||
| `getInlineHostArea()` | zone de lecture visée : override de rendu, pane active en vue fractionnée, sinon `#content-area` |
|
||||
| `isInlineEditorActive()` | une session occupe-t-elle une zone de contenu ? |
|
||||
| `mountEditorInline(area)` | `area.replaceChildren(container)` + classes `editor-inline` / `editor-inline-host` / `editor-inline-mode` |
|
||||
| `unmountEditorInline()` | conteneur rendu à la modale, zone vidée, renvoie la zone à re-rendre |
|
||||
|
||||
Aucun identifiant DOM n'est modifié (`#editor-body`, `#editor-save`, `#editor-title-input`…) :
|
||||
CodeMirror, l'auto-sauvegarde (2 s), la collaboration Yjs, la barre IA, l'aperçu Mermaid et
|
||||
l'iframe Forge continuent de fonctionner sans réécriture. Seul l'**hôte** change.
|
||||
|
||||
L'overlay reste monté et garde la classe `active` (signal d'état utilisé par l'auto-save, le
|
||||
raccourci Ctrl+J, la hauteur d'éditeur mobile…), mais passe en classe
|
||||
`editor-inline-mode` : transparent, sans fond ni padding, `pointer-events: none` (le ruban
|
||||
d'édition mobile `.me-ribbon`, ancré à la modale, conserve `pointer-events: auto`).
|
||||
|
||||
### 2. Cycle de vie
|
||||
|
||||
| Action | Effet |
|
||||
|---|---|
|
||||
| **Editer** / **Forge** | `activateInlineEditor(vault, path)` → bascule inline **si** la cible est le document affiché dans la zone de contenu |
|
||||
| ✓ (Sauvegarder) | sauvegarde puis `closeEditor()` → retour à la lecture (contenu relu depuis le disque) |
|
||||
| ✕ (Annuler) / **Échap** | `closeEditor()` → retour à la lecture (les modifications non sauvegardées sont abandonnées, comme avant) |
|
||||
| Ouverture d'un autre fichier (`renderFile()`) | garde-fou : `detachInlineEditor()` libère la session (destroy CodeMirror/Yjs, retrait de l'iframe Forge) avant le rendu de la lecture |
|
||||
| `forge-close` (message de l'iframe) | route vers `closeEditor()` — plus de manipulation manuelle de la modale dans `sync.js` |
|
||||
|
||||
**Repli overlay** : si la cible n'est pas le document affiché (fichier venant d'être créé via la
|
||||
palette, par exemple), la modale plein écran est conservée. Le mode inline exige que la zone de
|
||||
lecture soit celle du fichier édité — c'est ce qui garantit que « remplacer » signifie bien
|
||||
remplacer *ce* document.
|
||||
|
||||
L'invalidation du cache d'onglet (`_invalidateActiveTabCache`) évite qu'un aller-retour
|
||||
d'onglets réaffiche le HTML **d'avant** l'édition.
|
||||
|
||||
### 3. Assistant IA — document mis à jour sous les yeux de l'utilisateur
|
||||
|
||||
L'éditeur n'occupe plus le panneau de l'assistant : les deux surfaces cohabitent. Trois
|
||||
ajouts rendent la mise à jour effective de bout en bout :
|
||||
|
||||
1. **`app_context.editing`** — `bookslm.js::_editingDocument()` envoie
|
||||
`{vault, path, surface: "editor"|"forge"}` quand une session d'édition est ouverte ;
|
||||
`backend/bookslm.py::_format_app_context()` l'annonce dans le prompt Général
|
||||
(« Document en cours d'édition dans … ») et indique au modèle d'utiliser les outils
|
||||
d'écriture pour le mettre à jour.
|
||||
2. **`obsigate:file-written`** — à chaque événement SSE `tool` réussi d'un outil d'écriture
|
||||
(`edit_file`, `append_to_file`, `create_file`, `restore_backup`), `bookslm.js` diffuse un
|
||||
événement `{vault, path}`.
|
||||
3. **Rechargement** — `utils.js::reloadExternalWrite()` :
|
||||
- fichier ouvert dans l'éditeur inline → le tampon CodeMirror est **remplacé** par le contenu
|
||||
du disque (l'auto-sauvegarde de ce remplacement est neutralisée, sinon elle réécrivait
|
||||
l'ancien texte par-dessus la modification de l'IA) ;
|
||||
- fichier ouvert dans Forge → message `parent-reload` à l'iframe (`editor-poc.html`), qui
|
||||
relit le fichier et abandonne son tampon périmé ;
|
||||
- fichier simplement affiché en lecture → re-rendu depuis le disque.
|
||||
|
||||
Sans ce point 3, une édition par l'IA aurait été silencieusement écrasée par l'auto-sauvegarde de
|
||||
l'éditeur deux secondes plus tard.
|
||||
|
||||
### 4. Feuille de style
|
||||
|
||||
`frontend/style.css` : `.content-area.editor-inline-host` devient une colonne flex sans padding,
|
||||
et `.editor-container.editor-inline` la remplit (`flex: 1`, `height: auto` — pour battre le
|
||||
`height: 100vh` de la media query mobile par spécificité, `max-width/max-height: none`, sans
|
||||
bordure ni ombre). L'en-tête d'édition (titre, nom de fichier, pastille d'état, ✓/✕/🗑) sert
|
||||
donc de barre d'outils du document, et le corps occupe la zone de lecture.
|
||||
|
||||
## Limites connues
|
||||
|
||||
- Les outils de **renommage / déplacement** de l'IA (`rename_file`, `move_path`) ne déclenchent
|
||||
pas de rechargement : après un renommage, le document redevient accessible via l'arborescence.
|
||||
- La collaboration Yjs reste liée à l'ouverture de l'éditeur : un changement d'onglet pendant
|
||||
une session collaborative la termine (comportement identique à celui de la fermeture).
|
||||
- Le mode inline suppose un document affiché correspondant à la cible ; les flux « création de
|
||||
fichier » passent donc toujours par la modale.
|
||||
|
||||
## Vérification
|
||||
|
||||
```bash
|
||||
node tests/frontend/editor-inline.test.mjs # 19 tests (DOM + câblage)
|
||||
node tests/frontend/validate-imports.mjs
|
||||
node tests/frontend/unit.test.mjs
|
||||
.venv/Scripts/python.exe -m pytest tests/test_bookslm.py -k GeneralPrompt
|
||||
```
|
||||
|
||||
Contrôle manuel sur l'instance de test (`http://localhost:2020`, `admin` / `test123`) : ouvrir un
|
||||
document markdown → **Editer** (le document est remplacé par l'éditeur, bandeau/onglets toujours
|
||||
visibles, panneau assistant cliquable) → **Forge** depuis la lecture (iframe plein cadre) →
|
||||
✓ / ✕ / Échap → retour en lecture avec le contenu à jour.
|
||||
Reference in New Issue
Block a user