feat(forge): assistant IA partage et plein ecran Forge/Editer (#101)
This commit is contained in:
+21
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.7.5**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.8.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,26 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.8.0] — 2026-09-17
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
|
||||||
|
- **#101 - Forge : Assistant IA partagé et plein écran (Forge & Editer)** : le bouton
|
||||||
|
« AI Panel » de Forge ouvre désormais le **panneau Assistant IA** existant (barre latérale)
|
||||||
|
au lieu d'un mini-chat isolé — même contenu, même fournisseur/modèle, même historique, mêmes
|
||||||
|
menus `/` et `@`, sans duplication. Forge lit la sélection du sélecteur de l'assistant
|
||||||
|
(`localStorage['obsigate_ai_picker']`, même origine) et l'injecte dans ses appels
|
||||||
|
`/api/ai/*` et sa **complétion fantôme** (repli `ollama` si aucun fournisseur choisi) ; au
|
||||||
|
passage, les endpoints erronés sont corrigés (`make-longer`/`make-shorter`, `target_lang`).
|
||||||
|
Un bouton **plein écran** natif est ajouté à Forge (iframe `allow="fullscreen"`) et à
|
||||||
|
l'éditeur **Editer** (plein écran sur le conteneur, fonctionne en modale comme en inline,
|
||||||
|
sortie à la fermeture). Libellés i18n FR/EN. Fichiers : `frontend/editor-poc.html`,
|
||||||
|
`frontend/js/sync.js`, `frontend/js/viewer.js`, `frontend/js/utils.js`,
|
||||||
|
`frontend/index.html`, `frontend/style.css`, `frontend/locales/{fr,en}.json`. Tests :
|
||||||
|
`tests/frontend/editor-inline.test.mjs` (+10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.7.5] — 2026-09-17
|
## [2.7.5] — 2026-09-17
|
||||||
|
|
||||||
### Corrigé
|
### Corrigé
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -916,8 +916,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.7.5).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.8.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.7.5 | Dernière mise à jour : Juin 2026*
|
*Projet : ObsiGate | Version : 2.8.0 | Dernière mise à jour : Juin 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1085,8 +1085,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.7.5).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.8.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.7.5 | Last updated: May 2026*
|
*Project: ObsiGate | Version: 2.8.0 | Last updated: May 2026*
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.7.5"
|
version = "2.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.7.5"
|
version = "2.8.0"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.7.5",
|
"version": "2.8.0",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| 2026-09-16 | BUG-052 | Correction | `backend/agent/loop.py`, `tests/test_agent_loop.py`, `CHANGELOG.md` | **BUG-052** : la boucle d'agent ne rendait plus jamais de réponse vide. `_finalize_answer` : à l'épuisement du budget d'itérations (`STOP_MAX_ITERATIONS`) ou du quota d'appels (`STOP_QUOTA_EXCEEDED`), un dernier appel LLM **sans outil** reçoit une instruction de synthèse (« N'appelle plus aucun outil. Réponds maintenant… ») et son texte devient la réponse ; si l'appel échoue ou reste vide, `_fallback_summary` compose une liste déterministe des sources (`web_search`/`fetch_url`) pour ne jamais renvoyer un tour vide. Les `tool_calls` non atteints lors d'un arrêt sur quota reçoivent un résultat `deferred` (conversation valide pour la synthèse). Vérifié : `tests/test_agent_loop.py` 16/16 (+2 : synthèse finale, repli sources), suite complète 1084 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/agent/loop.py`). | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-09-16 | BUG-052 | Correction | `backend/agent/loop.py`, `tests/test_agent_loop.py`, `CHANGELOG.md` | **BUG-052** : la boucle d'agent ne rendait plus jamais de réponse vide. `_finalize_answer` : à l'épuisement du budget d'itérations (`STOP_MAX_ITERATIONS`) ou du quota d'appels (`STOP_QUOTA_EXCEEDED`), un dernier appel LLM **sans outil** reçoit une instruction de synthèse (« N'appelle plus aucun outil. Réponds maintenant… ») et son texte devient la réponse ; si l'appel échoue ou reste vide, `_fallback_summary` compose une liste déterministe des sources (`web_search`/`fetch_url`) pour ne jamais renvoyer un tour vide. Les `tool_calls` non atteints lors d'un arrêt sur quota reçoivent un résultat `deferred` (conversation valide pour la synthèse). Vérifié : `tests/test_agent_loop.py` 16/16 (+2 : synthèse finale, repli sources), suite complète 1084 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/agent/loop.py`). | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
| 2026-09-16 | BUG-053 | Correction | `backend/bookslm.py`, `backend/bookslm_routes.py`, `tests/test_bookslm.py`, `CHANGELOG.md` | **BUG-053** : en mode agent, le prompt Général (et dossier vide) enseignait le protocole texte `obsigate-action` ; le modèle décrivait donc l'action au lieu d'appeler l'outil natif `create_file` (bloc volumineux de surcroît tronqué avant fermeture → aucun fichier créé). Le prompt est scindé : `GENERAL_ACTION_TOOL_PROTOCOL` (appel direct des outils natifs, interdiction explicite des blocs `obsigate-action`) pour `build_general_system_prompt(agent=True)`, le protocole texte restant utilisé par le chat classique ; `_resolve_system_prompt` propage `agent` et ajoute une règle « Mode agent » aux prompts dossier/documents ; `max_tokens` de l'agent porté à 8192 pour un contenu de fichier complet. Vérifié : `tests/test_bookslm.py` 68/68 (+3 : prompt agent sans protocole texte, prompt classique inchangé, prompt système de l'endpoint agent), suite complète 1087 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/bookslm.py`, `backend/bookslm_routes.py`). | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-09-16 | BUG-053 | Correction | `backend/bookslm.py`, `backend/bookslm_routes.py`, `tests/test_bookslm.py`, `CHANGELOG.md` | **BUG-053** : en mode agent, le prompt Général (et dossier vide) enseignait le protocole texte `obsigate-action` ; le modèle décrivait donc l'action au lieu d'appeler l'outil natif `create_file` (bloc volumineux de surcroît tronqué avant fermeture → aucun fichier créé). Le prompt est scindé : `GENERAL_ACTION_TOOL_PROTOCOL` (appel direct des outils natifs, interdiction explicite des blocs `obsigate-action`) pour `build_general_system_prompt(agent=True)`, le protocole texte restant utilisé par le chat classique ; `_resolve_system_prompt` propage `agent` et ajoute une règle « Mode agent » aux prompts dossier/documents ; `max_tokens` de l'agent porté à 8192 pour un contenu de fichier complet. Vérifié : `tests/test_bookslm.py` 68/68 (+3 : prompt agent sans protocole texte, prompt classique inchangé, prompt système de l'endpoint agent), suite complète 1087 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/bookslm.py`, `backend/bookslm_routes.py`). | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
| 2026-09-17 | BUG-054 | Correction | `frontend/js/utils.js`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-054** : le bouton `#editor-save` (nœud partagé entre toutes les sessions d'édition) restait bloqué sur le spinner de chargement et désactivé — une sauvegarde manuelle (clic ou Ctrl+S) remplaçait le crochet par le loader et ne le restaurait jamais : succès (l'éditeur se ferme, la réouverture réaffichait le spinner), sauvegarde Forge, ou échec réseau (le `catch` ne restaurait ni l'icône ni l'état). Nouveau helper `resetSaveButton()` (crochet `✓`, `disabled=false`, styles en ligne nettoyés) appelé à l'ouverture (`openEditor`), à la fermeture (`closeEditor`) et en cas d'échec (`saveFile`). Vérifié : `tests/frontend/editor-inline.test.mjs` 29/29 (+4), validate-imports 38 modules / 0 erreur. | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-09-17 | BUG-054 | Correction | `frontend/js/utils.js`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-054** : le bouton `#editor-save` (nœud partagé entre toutes les sessions d'édition) restait bloqué sur le spinner de chargement et désactivé — une sauvegarde manuelle (clic ou Ctrl+S) remplaçait le crochet par le loader et ne le restaurait jamais : succès (l'éditeur se ferme, la réouverture réaffichait le spinner), sauvegarde Forge, ou échec réseau (le `catch` ne restaurait ni l'icône ni l'état). Nouveau helper `resetSaveButton()` (crochet `✓`, `disabled=false`, styles en ligne nettoyés) appelé à l'ouverture (`openEditor`), à la fermeture (`closeEditor`) et en cas d'échec (`saveFile`). Vérifié : `tests/frontend/editor-inline.test.mjs` 29/29 (+4), validate-imports 38 modules / 0 erreur. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
| 2026-09-17 | #101 | Feature | `frontend/editor-poc.html`, `frontend/js/sync.js`, `frontend/js/viewer.js`, `frontend/js/utils.js`, `frontend/index.html`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/editor-inline.test.mjs`, `docs/features/forge-assistant.md` (nouvelle), `docs/ROADMAP.md`, `CHANGELOG.md` | **#101** : le bouton « AI Panel » de Forge ouvre désormais l'**Assistant IA** partagé (`postMessage forge-open-ai` → `bookslm.openForCurrentContext()`) au lieu du mini-chat isolé (supprimé) ; Forge lit `localStorage['obsigate_ai_picker']` (`aiPickerSelection()`) pour ses appels `/api/ai/*` et sa complétion fantôme (repli `ollama`), endpoints corrigés (`make-longer`/`make-shorter`, `target_lang`) ; bouton **plein écran** natif ajouté à Forge (iframe `allow="fullscreen"`) et à Editer (`#editor-fullscreen`, conteneur `#editor-container`, sortie à la fermeture, Échap laissé au navigateur) ; i18n `editor.fullscreen`/`editor.exit_fullscreen`. Vérifié : `tests/frontend/editor-inline.test.mjs` 40/40 (+10), unit 9/9, validate-imports 38 modules, 13 suites JSDOM vertes. | 🟢 livré (en attente vérif utilisateur) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.7.5 | **Dernière mise à jour :** 2026-09-17
|
> **Version :** 2.8.0 | **Dernière mise à jour :** 2026-09-17
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -198,6 +198,7 @@
|
|||||||
| 98 | Assistant IA — Filtre de recherche dans la sidebar « Historique IA » | 2.5.0 | [features/ai-assistant-history.md](./features/ai-assistant-history.md) |
|
| 98 | Assistant IA — Filtre de recherche dans la sidebar « Historique IA » | 2.5.0 | [features/ai-assistant-history.md](./features/ai-assistant-history.md) |
|
||||||
| 99 | Sidebar — Filtrage des vues « Récents » et « Sauvegardes » | 2.6.0 | [features/sidebar-filters.md](./features/sidebar-filters.md) |
|
| 99 | Sidebar — Filtrage des vues « Récents » et « Sauvegardes » | 2.6.0 | [features/sidebar-filters.md](./features/sidebar-filters.md) |
|
||||||
| 100 | Assistant IA — Deep Research en pastille (au lieu du texte injecté) | 2.6.0 | [features/ai-assistant-history.md](./features/ai-assistant-history.md) |
|
| 100 | Assistant IA — Deep Research en pastille (au lieu du texte injecté) | 2.6.0 | [features/ai-assistant-history.md](./features/ai-assistant-history.md) |
|
||||||
|
| 101 | Forge — Assistant IA partagé (bouton AI Panel = assistant, fournisseur/modèle configuré, autocomplétion) + plein écran Forge/Editer | 2.8.0 | [features/forge-assistant.md](./features/forge-assistant.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# #101 - Forge : Assistant IA partagé + plein écran (Forge & Editer)
|
||||||
|
|
||||||
|
> **Statut :** 🟢 livré (en attente de vérification utilisateur)
|
||||||
|
> **Version :** 2.8.0
|
||||||
|
> **Composants :** `frontend/editor-poc.html`, `frontend/js/sync.js`,
|
||||||
|
> `frontend/js/viewer.js`, `frontend/js/utils.js`, `frontend/index.html`,
|
||||||
|
> `frontend/style.css`, `frontend/locales/{fr,en}.json`
|
||||||
|
> **Tests :** `tests/frontend/editor-inline.test.mjs` (+10)
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
L'éditeur **Forge** (`frontend/editor-poc.html`, iframe même origine) embarque son propre
|
||||||
|
mini-panneau « AI Panel » : un chat rudimentaire qui appelait `/api/ai/improve` **sans
|
||||||
|
fournisseur ni modèle** (donc toujours le défaut serveur), sans historique, sans streaming,
|
||||||
|
sans commandes `/` ni contexte `@`. L'éditeur **Editer** (CodeMirror) n'avait aucun bouton
|
||||||
|
plein écran, et Forge non plus.
|
||||||
|
|
||||||
|
L'utilisateur veut :
|
||||||
|
|
||||||
|
1. que le bouton « AI Panel » de Forge affiche **le même contenu** que le panneau
|
||||||
|
**Assistant IA** (fournisseur/modèle, historique, skills `/`, contexte `@`) ;
|
||||||
|
2. que Forge utilise le **fournisseur et le modèle configurés** dans l'Assistant IA
|
||||||
|
(actions IA, autocomplétion fantôme) ;
|
||||||
|
3. un bouton **plein écran** dans Forge **et** dans Editer.
|
||||||
|
|
||||||
|
## Conception
|
||||||
|
|
||||||
|
### 1. Forge ouvre l'Assistant IA existant (pas de duplication)
|
||||||
|
|
||||||
|
`bookslm.js` est un singleton monté sur le document parent, couplé à `state`, `TabManager`,
|
||||||
|
`AuthManager` et au CSS global : le porter dans l'iframe serait une duplication lourde à
|
||||||
|
maintenir. Forge étant une **iframe même origine**, son bouton AI se contente de demander au
|
||||||
|
parent d'ouvrir l'assistant :
|
||||||
|
|
||||||
|
| Côté | Mécanisme |
|
||||||
|
|---|---|
|
||||||
|
| Iframe | `openAssistant()` → `parent.postMessage({ type: 'forge-open-ai' }, '*')` (`#btn-ai`, `Ctrl+J`) |
|
||||||
|
| Parent (`sync.js`) | sur `forge-open-ai` : `import('./bookslm.js')` → `openForCurrentContext()` |
|
||||||
|
|
||||||
|
Le mini-panneau Forge (`#ai-panel`, `sendAIChat`, suggestions) est **supprimé** : le contenu,
|
||||||
|
le fournisseur/modèle, l'historique, les menus `/` et `@` sont ceux de l'assistant, sans
|
||||||
|
double maintenance.
|
||||||
|
|
||||||
|
### 2. Fournisseur/modèle partagé
|
||||||
|
|
||||||
|
Le sélecteur de l'assistant persiste son choix dans `localStorage['obsigate_ai_picker']`
|
||||||
|
(`{provider, model}`), clé **partagée** par l'iframe (même origine). Forge la lit via
|
||||||
|
`aiPickerSelection()` et l'injecte :
|
||||||
|
|
||||||
|
- dans `aiCall()` (actions IA du menu `/` et de la bulle de sélection) ;
|
||||||
|
- dans la **complétion fantôme** (`/api/ai/inline-complete`) — repli `ollama` si aucun
|
||||||
|
fournisseur n'est sélectionné (comportement local conservé).
|
||||||
|
|
||||||
|
Les noms d'endpoints erronés sont corrigés au passage : `make-longer` / `make-shorter`
|
||||||
|
(au lieu de `lengthen` / `simplify`) et `target_lang` pour la traduction.
|
||||||
|
|
||||||
|
### 3. Plein écran natif
|
||||||
|
|
||||||
|
- **Forge** : bouton `#btn-fullscreen` dans la barre, `document.documentElement.requestFullscreen()`
|
||||||
|
(l'iframe reçoit `allow="fullscreen"` côté parent, `viewer.js`), icône basculée sur
|
||||||
|
`fullscreenchange`.
|
||||||
|
- **Editer** : bouton `#editor-fullscreen` dans l'en-tête ; plein écran sur le conteneur
|
||||||
|
`#editor-container` (`getEditorContainer()`, fonctionne en mode modale **et** inline),
|
||||||
|
styles `:fullscreen` (`width/height: 100vw/100vh`), icône/label basculés, sortie du plein
|
||||||
|
écran à la fermeture (`closeEditor`) et Échap laissé au navigateur pendant le plein écran.
|
||||||
|
|
||||||
|
Libellés i18n `editor.fullscreen` / `editor.exit_fullscreen` (FR + EN).
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
| Fichier | Modification |
|
||||||
|
|---|---|
|
||||||
|
| `frontend/editor-poc.html` | suppression du mini-panneau AI ; `openAssistant()` (postMessage) ; `aiPickerSelection()` injecté dans `aiCall`/complétion fantôme ; `AI_MAP` corrigé ; bouton + logique plein écran |
|
||||||
|
| `frontend/js/sync.js` | routage `forge-open-ai` → `bookslm.openForCurrentContext()` |
|
||||||
|
| `frontend/js/viewer.js` | `allow="fullscreen"` sur `#forge-iframe` |
|
||||||
|
| `frontend/index.html` | bouton `#editor-fullscreen` (titre i18n) |
|
||||||
|
| `frontend/js/utils.js` | `toggleEditorFullscreen` / `updateFullscreenButton` / `isEditorFullscreen` ; sortie du plein écran dans `closeEditor` |
|
||||||
|
| `frontend/style.css` | `.editor-container:fullscreen` |
|
||||||
|
| `frontend/locales/{fr,en}.json` | `editor.fullscreen`, `editor.exit_fullscreen` |
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
`tests/frontend/editor-inline.test.mjs` (+10) : suppression du mini-panneau, postMessage
|
||||||
|
`forge-open-ai`, routage `sync.js`, lecture de `obsigate_ai_picker`, endpoints corrigés,
|
||||||
|
complétion fantôme, boutons plein écran (Forge + Editer), `allow="fullscreen"`, CSS
|
||||||
|
`:fullscreen`, clés i18n.
|
||||||
+65
-88
@@ -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-content img { max-width: 100%; border-radius: var(--r); }
|
||||||
.preview-loading { text-align: center; padding: 40px; color: var(--text3); font-size: 13px; }
|
.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 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 { 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; }
|
.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">
|
<div class="bar-right">
|
||||||
<button class="btn" id="btn-help" title="Aide / Raccourcis (F1)">?</button>
|
<button class="btn" id="btn-help" title="Aide / Raccourcis (F1)">?</button>
|
||||||
<button class="btn" id="btn-preview" title="Toggle preview">👁</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 class="save-dot ok" id="save-dot"><span class="dot"></span><span id="save-label">Saved</span></div>
|
||||||
</div>
|
</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 class="bubble-more-item" data-action="ai-continue">➕ Continue writing</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Status bar -->
|
<!-- Status bar -->
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="stat-left">
|
<div class="stat-left">
|
||||||
<span>Type <strong>/</strong> for commands | <strong>Alt+\</strong> autocomplete | <strong>Alt+I</strong> Insert</span>
|
<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+S save</span>
|
||||||
<span>Ctrl+K link</span>
|
<span>Ctrl+K link</span>
|
||||||
<span class="stat-ai" id="stat-ai">✨ AI processing...</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>
|
<table>
|
||||||
<tr><td>Ctrl+S</td><td>Sauvegarder</td></tr>
|
<tr><td>Ctrl+S</td><td>Sauvegarder</td></tr>
|
||||||
<tr><td>Clic sur le titre</td><td>Renommer le fichier</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>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)</td><td>Continue la liste (-, *, 1.) et checkboxes</td></tr>
|
||||||
<tr><td>Enter (liste vide)</td><td>Termine la liste</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 bubble = document.getElementById('bubble');
|
||||||
var bubbleMore = document.getElementById('bubble-more-menu');
|
var bubbleMore = document.getElementById('bubble-more-menu');
|
||||||
var qiMenu = document.getElementById('qi-menu');
|
var qiMenu = document.getElementById('qi-menu');
|
||||||
var aiPanel = document.getElementById('ai-panel');
|
|
||||||
var btnAI = document.getElementById('btn-ai');
|
var btnAI = document.getElementById('btn-ai');
|
||||||
var btnPreview = document.getElementById('btn-preview');
|
var btnPreview = document.getElementById('btn-preview');
|
||||||
var saveDot = document.getElementById('save-dot');
|
var saveDot = document.getElementById('save-dot');
|
||||||
@@ -699,6 +658,21 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- AI calls ----
|
// ---- 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) {
|
function aiCall(endpoint, text, extra, callback) {
|
||||||
if (!aiConfigured) {
|
if (!aiConfigured) {
|
||||||
// Check status first, then retry
|
// Check status first, then retry
|
||||||
@@ -712,6 +686,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
}
|
}
|
||||||
showAIProcessing();
|
showAIProcessing();
|
||||||
var body = { text: text };
|
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]; });
|
if (extra) Object.keys(extra).forEach(function(k) { body[k] = extra[k]; });
|
||||||
fetch('/api/ai/' + endpoint, {
|
fetch('/api/ai/' + endpoint, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
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() {
|
function updateAIButton() {
|
||||||
if (aiConfigured) {
|
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';
|
document.getElementById('stat-ai-ready').style.display = 'inline';
|
||||||
} else {
|
} 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';
|
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-fix': { ep: 'fix-spelling', desc: 'fix spelling' },
|
||||||
'ai-continue': { ep: 'continue', desc: 'continue writing' },
|
'ai-continue': { ep: 'continue', desc: 'continue writing' },
|
||||||
'ai-summarize': { ep: 'summarize', desc: 'summarize' },
|
'ai-summarize': { ep: 'summarize', desc: 'summarize' },
|
||||||
'ai-translate': { ep: 'translate', extra: { language: 'en' }, desc: 'translate' },
|
'ai-translate': { ep: 'translate', extra: { target_lang: 'en' }, desc: 'translate' },
|
||||||
'ai-shorter': { ep: 'simplify', desc: 'make shorter' },
|
'ai-shorter': { ep: 'make-shorter', desc: 'make shorter' },
|
||||||
'ai-longer': { ep: 'lengthen', desc: 'make longer' },
|
'ai-longer': { ep: 'make-longer', desc: 'make longer' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function execAIAction(actionKey) {
|
function execAIAction(actionKey) {
|
||||||
@@ -1173,36 +1149,38 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
showQI();
|
showQI();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- AI Panel ----
|
// ---- AI Assistant (#101) ----
|
||||||
function toggleAIPanel() {
|
// The rich assistant (provider/model picker, streaming, `/` skills, `@`
|
||||||
aiPanel.classList.toggle('hidden');
|
// context, history) lives in the parent application. Forge is a same-origin
|
||||||
btnAI.classList.toggle('active', !aiPanel.classList.contains('hidden'));
|
// 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() {
|
// ---- Fullscreen (#101) ----
|
||||||
var input = document.getElementById('ai-input');
|
var btnFullscreen = document.getElementById('btn-fullscreen');
|
||||||
var chat = document.getElementById('ai-chat');
|
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 text = input.value.trim();
|
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>';
|
||||||
if (!text) return;
|
|
||||||
input.value = '';
|
|
||||||
|
|
||||||
var um = document.createElement('div'); um.className = 'ai-msg user'; um.textContent = text;
|
function toggleFullscreen() {
|
||||||
chat.appendChild(um);
|
if (!document.fullscreenElement) {
|
||||||
|
var req = document.documentElement.requestFullscreen && document.documentElement.requestFullscreen();
|
||||||
var lm = document.createElement('div'); lm.className = 'ai-msg loading';
|
if (req && req.catch) req.catch(function() { showToast('Plein écran indisponible', 'err'); });
|
||||||
lm.innerHTML = '<span class="spin"></span>Thinking...';
|
} else if (document.exitFullscreen) {
|
||||||
chat.appendChild(lm);
|
document.exitFullscreen();
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ----
|
// ---- Drag & Drop ----
|
||||||
var dragC = 0;
|
var dragC = 0;
|
||||||
document.addEventListener('dragenter', function(e) { e.preventDefault(); dragC++; if (dragC === 1) dropOverlay.classList.add('on'); });
|
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);
|
var before = l.text.substring(0, p.s - l.start);
|
||||||
if (before === '' || /^\s*$/.test(before)) setTimeout(showSlash, 20);
|
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 === 's') { e.preventDefault(); forceSave(); }
|
||||||
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
|
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
|
||||||
if (e.ctrlKey && e.key === 'i') { 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(); }
|
if (langPicker.classList.contains('on') && !langPicker.contains(e.target)) { langPicker.classList.remove('on'); ta.focus(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
btnAI.addEventListener('click', toggleAIPanel);
|
btnAI.addEventListener('click', openAssistant);
|
||||||
document.getElementById('ai-close').addEventListener('click', toggleAIPanel);
|
btnFullscreen.addEventListener('click', toggleFullscreen);
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
btnPreview.addEventListener('click', togglePreview);
|
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;
|
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', {
|
fetch('/api/ai/inline-complete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||||||
.then(function(data) {
|
.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('ObsiGate Editor v2 ready');
|
||||||
console.log(' File: ' + (fileVault ? fileVault + '/' + filePath : 'demo mode'));
|
console.log(' File: ' + (fileVault ? fileVault + '/' + filePath : 'demo mode'));
|
||||||
console.log(' AI: checking...');
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1436,6 +1436,15 @@
|
|||||||
><span class="dot"></span
|
><span class="dot"></span
|
||||||
><span id="editor-save-label">Saved</span></span
|
><span id="editor-save-label">Saved</span></span
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
class="editor-btn"
|
||||||
|
id="editor-fullscreen"
|
||||||
|
title="Plein écran"
|
||||||
|
aria-label="Plein écran"
|
||||||
|
data-i18n-attr="title:editor.fullscreen"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" 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>
|
||||||
<button
|
<button
|
||||||
class="editor-btn danger"
|
class="editor-btn danger"
|
||||||
id="editor-delete"
|
id="editor-delete"
|
||||||
|
|||||||
@@ -645,5 +645,15 @@ export function init() {
|
|||||||
// (inline mode) and re-renders the document read view.
|
// (inline mode) and re-renders the document read view.
|
||||||
closeEditor();
|
closeEditor();
|
||||||
}
|
}
|
||||||
|
if (e.data.type === 'forge-open-ai') {
|
||||||
|
// #101 — Forge has no rich AI panel of its own: its AI button asks the
|
||||||
|
// parent to open the shared AI Assistant (same provider/model, skills,
|
||||||
|
// context and history).
|
||||||
|
import('./bookslm.js').then(function(m) {
|
||||||
|
if (m && m.default) m.default.openForCurrentContext();
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.warn('[Forge] Unable to open AI assistant', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-2
@@ -1,5 +1,6 @@
|
|||||||
import { state } from './state.js';
|
import { state } from './state.js';
|
||||||
import { api } from './auth.js';
|
import { api } from './auth.js';
|
||||||
|
import { t } from './i18n.js';
|
||||||
import { openFile, showWelcome } from './viewer.js';
|
import { openFile, showWelcome } from './viewer.js';
|
||||||
import { refreshSidebarForContext, refreshTagsForContext } from './sidebar.js';
|
import { refreshSidebarForContext, refreshTagsForContext } from './sidebar.js';
|
||||||
import { createAIToolbar } from './ai.js';
|
import { createAIToolbar } from './ai.js';
|
||||||
@@ -586,6 +587,11 @@ function closeEditor() {
|
|||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
modal.classList.remove("active");
|
modal.classList.remove("active");
|
||||||
resetSaveButton();
|
resetSaveButton();
|
||||||
|
// Leaving the editor must also leave native fullscreen. #101
|
||||||
|
if (isEditorFullscreen() && document.exitFullscreen) {
|
||||||
|
const fsPromise = document.exitFullscreen();
|
||||||
|
if (fsPromise && fsPromise.catch) fsPromise.catch(() => {});
|
||||||
|
}
|
||||||
stopCollab();
|
stopCollab();
|
||||||
if (state.editorView) {
|
if (state.editorView) {
|
||||||
state.editorView.destroy();
|
state.editorView.destroy();
|
||||||
@@ -832,19 +838,65 @@ async function deleteFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fullscreen (#101)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const FS_ENTER_SVG = '<svg width="16" height="16" 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>';
|
||||||
|
const FS_EXIT_SVG = '<svg width="16" height="16" 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>';
|
||||||
|
|
||||||
|
/** True when the editor container currently owns the fullscreen viewport. */
|
||||||
|
function isEditorFullscreen() {
|
||||||
|
if (!document.fullscreenElement) return false;
|
||||||
|
const container = getEditorContainer();
|
||||||
|
return !!(container && (document.fullscreenElement === container || container.contains(document.fullscreenElement)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle native fullscreen on the editor container (modal or inline). */
|
||||||
|
function toggleEditorFullscreen() {
|
||||||
|
const container = getEditorContainer();
|
||||||
|
if (!container) return;
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
const p = document.exitFullscreen();
|
||||||
|
if (p && p.catch) p.catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (container.requestFullscreen) {
|
||||||
|
const p = container.requestFullscreen();
|
||||||
|
if (p && p.catch) p.catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sync the fullscreen button icon/label with the current fullscreen state. */
|
||||||
|
function updateFullscreenButton() {
|
||||||
|
const btn = document.getElementById("editor-fullscreen");
|
||||||
|
if (!btn) return;
|
||||||
|
const on = isEditorFullscreen();
|
||||||
|
btn.innerHTML = on ? FS_EXIT_SVG : FS_ENTER_SVG;
|
||||||
|
const label = t(on ? "editor.exit_fullscreen" : "editor.fullscreen");
|
||||||
|
btn.title = label;
|
||||||
|
btn.setAttribute("aria-label", label);
|
||||||
|
btn.classList.toggle("active", on);
|
||||||
|
}
|
||||||
|
|
||||||
function initEditor() {
|
function initEditor() {
|
||||||
const cancelBtn = document.getElementById("editor-cancel");
|
const cancelBtn = document.getElementById("editor-cancel");
|
||||||
const deleteBtn = document.getElementById("editor-delete");
|
const deleteBtn = document.getElementById("editor-delete");
|
||||||
const saveBtn = document.getElementById("editor-save");
|
const saveBtn = document.getElementById("editor-save");
|
||||||
|
const fullscreenBtn = document.getElementById("editor-fullscreen");
|
||||||
const modal = document.getElementById("editor-modal");
|
const modal = document.getElementById("editor-modal");
|
||||||
|
|
||||||
cancelBtn.addEventListener("click", closeEditor);
|
cancelBtn.addEventListener("click", closeEditor);
|
||||||
deleteBtn.addEventListener("click", deleteFile);
|
deleteBtn.addEventListener("click", deleteFile);
|
||||||
saveBtn.addEventListener("click", () => saveFile());
|
saveBtn.addEventListener("click", () => saveFile());
|
||||||
|
if (fullscreenBtn) fullscreenBtn.addEventListener("click", toggleEditorFullscreen);
|
||||||
|
document.addEventListener("fullscreenchange", updateFullscreenButton);
|
||||||
|
|
||||||
// ESC to close
|
// ESC to close — unless we are in native fullscreen, where Escape exits
|
||||||
|
// fullscreen first (the editor must stay open). #101
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape" && modal.classList.contains("active")) {
|
if (e.key === "Escape" && !document.fullscreenElement && modal.classList.contains("active")) {
|
||||||
closeEditor();
|
closeEditor();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -781,6 +781,9 @@ export function renderFile(data) {
|
|||||||
var iframe = document.createElement("iframe");
|
var iframe = document.createElement("iframe");
|
||||||
iframe.id = "forge-iframe";
|
iframe.id = "forge-iframe";
|
||||||
iframe.src = "/editor-poc?vault=" + encodeURIComponent(data.vault) + "&path=" + encodeURIComponent(data.path) + "&_ts=" + Date.now();
|
iframe.src = "/editor-poc?vault=" + encodeURIComponent(data.vault) + "&path=" + encodeURIComponent(data.path) + "&_ts=" + Date.now();
|
||||||
|
// #101 — allow the Forge editor's own Fullscreen button to work.
|
||||||
|
iframe.setAttribute("allow", "fullscreen");
|
||||||
|
iframe.setAttribute("allowfullscreen", "");
|
||||||
// Inline: the iframe fills the content area; overlay: the original 82vh.
|
// Inline: the iframe fills the content area; overlay: the original 82vh.
|
||||||
iframe.style.cssText = "width:100%;height:" + (inline ? "100%" : "82vh") + ";border:none;display:block;";
|
iframe.style.cssText = "width:100%;height:" + (inline ? "100%" : "82vh") + ";border:none;display:block;";
|
||||||
bodyEl.appendChild(iframe);
|
bodyEl.appendChild(iframe);
|
||||||
|
|||||||
@@ -654,12 +654,14 @@
|
|||||||
"editor.delete_error": "Delete error",
|
"editor.delete_error": "Delete error",
|
||||||
"editor.edit": "Edit current file",
|
"editor.edit": "Edit current file",
|
||||||
"editor.edit_current_desc": "Open current file in editor",
|
"editor.edit_current_desc": "Open current file in editor",
|
||||||
|
"editor.exit_fullscreen": "Exit fullscreen",
|
||||||
"editor.find": "Find in file...",
|
"editor.find": "Find in file...",
|
||||||
"editor.find_case": "Case sensitive",
|
"editor.find_case": "Case sensitive",
|
||||||
"editor.find_regex": "Regex",
|
"editor.find_regex": "Regex",
|
||||||
"editor.find_whole": "Whole word",
|
"editor.find_whole": "Whole word",
|
||||||
"editor.forge_close_editor": "Close editor / modal",
|
"editor.forge_close_editor": "Close editor / modal",
|
||||||
"editor.forge_help": "Forge editor help",
|
"editor.forge_help": "Forge editor help",
|
||||||
|
"editor.fullscreen": "Fullscreen",
|
||||||
"editor.no_results": "No results",
|
"editor.no_results": "No results",
|
||||||
"editor.replace": "Replace",
|
"editor.replace": "Replace",
|
||||||
"editor.save": "Save",
|
"editor.save": "Save",
|
||||||
|
|||||||
@@ -654,12 +654,14 @@
|
|||||||
"editor.delete_error": "Erreur de suppression",
|
"editor.delete_error": "Erreur de suppression",
|
||||||
"editor.edit": "Éditer fichier courant",
|
"editor.edit": "Éditer fichier courant",
|
||||||
"editor.edit_current_desc": "Ouvrir le fichier actif dans l'éditeur",
|
"editor.edit_current_desc": "Ouvrir le fichier actif dans l'éditeur",
|
||||||
|
"editor.exit_fullscreen": "Quitter le plein écran",
|
||||||
"editor.find": "Rechercher dans le fichier...",
|
"editor.find": "Rechercher dans le fichier...",
|
||||||
"editor.find_case": "Respecter la casse",
|
"editor.find_case": "Respecter la casse",
|
||||||
"editor.find_regex": "Regex",
|
"editor.find_regex": "Regex",
|
||||||
"editor.find_whole": "Mot entier",
|
"editor.find_whole": "Mot entier",
|
||||||
"editor.forge_close_editor": "Fermer l'éditeur / modale",
|
"editor.forge_close_editor": "Fermer l'éditeur / modale",
|
||||||
"editor.forge_help": "Aide de l'éditeur Forge",
|
"editor.forge_help": "Aide de l'éditeur Forge",
|
||||||
|
"editor.fullscreen": "Plein écran",
|
||||||
"editor.no_results": "Aucun résultat",
|
"editor.no_results": "Aucun résultat",
|
||||||
"editor.replace": "Remplacer",
|
"editor.replace": "Remplacer",
|
||||||
"editor.save": "Enregistrer",
|
"editor.save": "Enregistrer",
|
||||||
|
|||||||
@@ -2796,6 +2796,23 @@ select {
|
|||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Native fullscreen (#101) — the editor fills the viewport regardless of the
|
||||||
|
inline/modal constraints above. */
|
||||||
|
.editor-container:fullscreen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.editor-container:fullscreen .editor-body,
|
||||||
|
.editor-container:fullscreen .editor-body-cm {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Inline edition (#93) --------------------------------------------------
|
/* --- Inline edition (#93) --------------------------------------------------
|
||||||
When a document is being edited, `#editor-container` is moved into the
|
When a document is being edited, `#editor-container` is moved into the
|
||||||
document content area: the editor *replaces* the read view instead of
|
document content area: the editor *replaces* the read view instead of
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.7.5",
|
"version": "2.8.0",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -381,6 +381,77 @@ test("utils.js saveFile restores the save button when the request fails", () =>
|
|||||||
assert.match(utilsSrc, /console\.error\("Save error:", err\);\s*\n\s*resetSaveButton\(\);/);
|
assert.match(utilsSrc, /console\.error\("Save error:", err\);\s*\n\s*resetSaveButton\(\);/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── #101: Forge uses the shared AI assistant + native fullscreen ──
|
||||||
|
test("editor-poc.html opens the shared AI assistant from its AI button", () => {
|
||||||
|
assert.ok(!/id="ai-panel"/.test(forgeSrc), "Forge mini AI panel markup must be gone");
|
||||||
|
assert.match(forgeSrc, /postMessage\(\{ type: 'forge-open-ai' \}, '\*'\)/);
|
||||||
|
assert.match(forgeSrc, /btnAI\.addEventListener\('click', openAssistant\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sync.js routes forge-open-ai to the BooksLM assistant", () => {
|
||||||
|
const handler = syncSrc.match(/if \(e\.data\.type === 'forge-open-ai'\) \{([\s\S]*?)\n \}/);
|
||||||
|
assert.ok(handler, "forge-open-ai handler not found");
|
||||||
|
assert.match(handler[1], /import\('\.\/bookslm\.js'\)/);
|
||||||
|
assert.match(handler[1], /openForCurrentContext\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("editor-poc.html AI calls use the assistant's configured provider/model", () => {
|
||||||
|
const pick = forgeSrc.match(/function aiPickerSelection\(\) \{([\s\S]*?)\n \}/);
|
||||||
|
assert.ok(pick, "aiPickerSelection not found");
|
||||||
|
assert.match(pick[1], /obsigate_ai_picker/);
|
||||||
|
assert.match(forgeSrc, /var pick = aiPickerSelection\(\);/);
|
||||||
|
assert.match(forgeSrc, /Object\.keys\(pick\)\.forEach/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("editor-poc.html fixes AI endpoint names and translate param", () => {
|
||||||
|
assert.match(forgeSrc, /'ai-longer':\s*\{ ep: 'make-longer'/);
|
||||||
|
assert.match(forgeSrc, /'ai-shorter':\s*\{ ep: 'make-shorter'/);
|
||||||
|
assert.match(forgeSrc, /'ai-translate':\s*\{ ep: 'translate', extra: \{ target_lang: 'en' \}/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("editor-poc.html ghost completion follows the configured provider", () => {
|
||||||
|
assert.match(forgeSrc, /ghostBody\.provider = ghostPick\.provider \|\| 'ollama';/);
|
||||||
|
assert.match(forgeSrc, /ghostBody\.model = ghostPick\.model;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("editor-poc.html has a native fullscreen button", () => {
|
||||||
|
assert.match(forgeSrc, /id="btn-fullscreen"/);
|
||||||
|
assert.match(forgeSrc, /document\.documentElement\.requestFullscreen/);
|
||||||
|
assert.match(forgeSrc, /document\.addEventListener\('fullscreenchange'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewer.js allows fullscreen in the Forge iframe", () => {
|
||||||
|
assert.match(viewerSrc, /iframe\.setAttribute\("allow", "fullscreen"\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("index.html has an editor fullscreen button with i18n title", () => {
|
||||||
|
assert.match(indexSrc, /id="editor-fullscreen"/);
|
||||||
|
assert.match(indexSrc, /data-i18n-attr="title:editor\.fullscreen"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("utils.js wires the editor fullscreen button and exits on close", () => {
|
||||||
|
assert.match(utilsSrc, /function toggleEditorFullscreen\(\)/);
|
||||||
|
assert.match(utilsSrc, /function updateFullscreenButton\(\)/);
|
||||||
|
assert.match(utilsSrc, /document\.addEventListener\("fullscreenchange", updateFullscreenButton\)/);
|
||||||
|
const close = utilsSrc.match(/function closeEditor\(\) \{([\s\S]*?)\n\}/);
|
||||||
|
assert.ok(close, "closeEditor not found");
|
||||||
|
assert.match(close[1], /document\.exitFullscreen\(\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("style.css makes the editor fill the screen in fullscreen", () => {
|
||||||
|
assert.match(cssSrc, /\.editor-container:fullscreen \{/);
|
||||||
|
assert.match(cssSrc, /\.editor-container:fullscreen \{[\s\S]*?height: 100vh;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("locales expose the fullscreen labels (FR + EN)", () => {
|
||||||
|
const fr = JSON.parse(read("frontend", "locales", "fr.json"));
|
||||||
|
const en = JSON.parse(read("frontend", "locales", "en.json"));
|
||||||
|
for (const key of ["editor.fullscreen", "editor.exit_fullscreen"]) {
|
||||||
|
assert.ok(fr[key], `fr.json missing ${key}`);
|
||||||
|
assert.ok(en[key], `en.json missing ${key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`\n${testCount - failCount}/${testCount} tests passed`);
|
console.log(`\n${testCount - failCount}/${testCount} tests passed`);
|
||||||
if (failCount > 0) {
|
if (failCount > 0) {
|
||||||
console.error(`${failCount} test(s) failed`);
|
console.error(`${failCount} test(s) failed`);
|
||||||
|
|||||||
Reference in New Issue
Block a user