From 3131277b19f6c9b74aa88d349cbdf99418f4df58 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 17 Sep 2026 08:30:29 -0400 Subject: [PATCH] feat(forge): assistant IA partage et plein ecran Forge/Editer (#101) --- CHANGELOG.md | 22 +++- README.fr.md | 6 +- README.md | 6 +- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ISSUES_TODOLIST.md | 1 + docs/ROADMAP.md | 3 +- docs/features/forge-assistant.md | 86 +++++++++++++++ frontend/editor-poc.html | 153 +++++++++++--------------- frontend/index.html | 9 ++ frontend/js/sync.js | 10 ++ frontend/js/utils.js | 56 +++++++++- frontend/js/viewer.js | 3 + frontend/locales/en.json | 2 + frontend/locales/fr.json | 2 + frontend/style.css | 17 +++ package.json | 2 +- tests/frontend/editor-inline.test.mjs | 71 ++++++++++++ 20 files changed, 354 insertions(+), 103 deletions(-) create mode 100644 docs/features/forge-assistant.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9bc3a..3de53a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). > **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 ### Corrigé diff --git a/README.fr.md b/README.fr.md index 96de4e6..2dff347 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.7.5-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.8.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -916,8 +916,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 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* diff --git a/README.md b/README.md index a533704..972c0b3 100644 --- a/README.md +++ b/README.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.7.5-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.8.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1085,8 +1085,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 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* diff --git a/VERSION b/VERSION index a603bb5..834f262 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.5 +2.8.0 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index f81583f..54a6cae 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.7.5" +version = "2.8.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 608fb41..7d44cfd 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.7.5" +version = "2.8.0" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 6ef476a..536fcd9 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.7.5", + "version": "2.8.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 3e8be55..3011214 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -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-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 | #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) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1a778ac..60a7362 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # 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 > vers les fonctionnalités livrées. > - **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) | | 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) | +| 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) | --- diff --git a/docs/features/forge-assistant.md b/docs/features/forge-assistant.md new file mode 100644 index 0000000..f2d2ede --- /dev/null +++ b/docs/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. diff --git a/frontend/editor-poc.html b/frontend/editor-poc.html index 54647c7..98cb99c 100644 --- a/frontend/editor-poc.html +++ b/frontend/editor-poc.html @@ -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
- + +
Saved
@@ -380,33 +360,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
➕ Continue writing
- - -
Type / for commands | Alt+\ autocomplete | Alt+I Insert - Ctrl+J AI panel + Ctrl+J Assistant IA Ctrl+S save Ctrl+K link ✨ AI processing... @@ -453,7 +413,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig - + @@ -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 = ''; + var FS_EXIT = ''; - 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 = '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'); })(); diff --git a/frontend/index.html b/frontend/index.html index 41275e0..8585221 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1436,6 +1436,15 @@ >Saved +
Ctrl+SSauvegarder
Clic sur le titreRenommer le fichier
Ctrl+JPanneau AI
Ctrl+JAssistant IA
Alt+\Autocompletion intelligente
Enter (liste)Continue la liste (-, *, 1.) et checkboxes
Enter (liste vide)Termine la liste