From 014df322e782c1ce64b7b546b3f71b9deb5d38fa Mon Sep 17 00:00:00 2001 From: Spark Date: Thu, 22 Oct 2026 12:02:09 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20panneau=20Spark=20v2=20=E2=80=94=20Mark?= =?UTF-8?q?down,=20largeur,=20th=C3=A8me,=20discussions=20#219?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 20 +- README.fr.md | 6 +- README.md | 6 +- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ROADMAP.md | 19 +- docs/features/fusion-panneau-spark-v2-219.md | 29 ++ frontend/draco-panneau.css | 180 +++++++-- frontend/draco-panneau.js | 391 ++++++++++++++++--- package.json | 2 +- 12 files changed, 565 insertions(+), 96 deletions(-) create mode 100644 docs/features/fusion-panneau-spark-v2-219.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ddccd4..3a512bb 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.74.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.75.0**. --- @@ -14,6 +14,24 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.75.0] — 2026-10-22 + +### Ajouté + +- **Panneau Spark v2 (#219, Fusion — Lot 1)** — les assets partagés + `frontend/draco-panneau.js` / `frontend/draco-panneau.css` passent + en v2, à l'identique dans les quatre applications : réponses de + l'agent en **Markdown rendu** (HTML produit par le service + d'agents 0.10.0, sanitizé côté serveur), **largeur réglable** + persistée, **thème clair/sombre** aligné sur l'application hôte + (suivi en direct de `html[data-theme]`), et **discussions façon + Muse** — discussion principale perpétuelle et discussions + parallèles créées, archivées, supprimées depuis le tiroir du + panneau. Aucun code propre à ObsiGate n'est modifié ; + redéploiement Docker requis. + +--- + ## [2.74.0] — 2026-10-22 ### Ajouté diff --git a/README.fr.md b/README.fr.md index 92d975a..0274fd8 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.74.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.75.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/) @@ -977,8 +977,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.74.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.75.0). --- -*Projet : ObsiGate | Version : 2.74.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.75.0 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 58fe4e2..e072caf 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.74.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.75.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/) @@ -1152,8 +1152,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.74.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.75.0). --- -*Project: ObsiGate | Version: 2.74.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.75.0 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 04b6fa2..ddcb9bf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.74.0 +2.75.0 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index aac3a23..0939222 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.74.0" +version = "2.75.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 5a521e2..9e4c705 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.74.0" +version = "2.75.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 228ec59..52391cb 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.74.0", + "version": "2.75.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f6c3fb8..6b661c0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.74.0 | **Dernière mise à jour :** 2026-10-22 +> **Version :** 2.75.0 | **Dernière mise à jour :** 2026-10-22 > **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)** @@ -48,6 +48,23 @@ (`backend/main.py`, seul hôte ajouté) - [x] Tests `test_draco_barre.py` (intégration + CSP verrouillées) +### 219. Fusion — panneau Spark v2 (Lot 1) + +- **Effort :** ~0,5 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-22 | **Statut :** 🔵 en cours +- **Fiche :** [features/fusion-panneau-spark-v2-219.md](./features/fusion-panneau-spark-v2-219.md) +- **Description :** le panneau Spark (#218) passe en v2, à l'identique + dans les quatre applications (carte fusion-trois-apps #17) : + réponses de l'agent en **Markdown rendu** (HTML produit par le + service d'agents 0.10.0, sanitizé côté serveur), **largeur + réglable** persistée, **thème clair/sombre aligné** sur + l'application hôte, **discussions façon Muse** (principale + perpétuelle + parallèles créées/archivées/supprimées). Côté + ObsiGate : assets vendangés uniquement, aucun code propre touché. +- **Sous-tâches :** + - [x] Assets `frontend/draco-panneau.js` / `frontend/draco-panneau.css` + remplacés par la v2 partagée du programme + - [x] Fiche de conception `features/fusion-panneau-spark-v2-219.md` + ### 215. Fusion bloc 1 — barre d'apps commune et références croisées des notes - **Effort :** ~1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-20 | **Statut :** ✅ livré le 2026-10-20 (2.70.0) diff --git a/docs/features/fusion-panneau-spark-v2-219.md b/docs/features/fusion-panneau-spark-v2-219.md new file mode 100644 index 0000000..baacb70 --- /dev/null +++ b/docs/features/fusion-panneau-spark-v2-219.md @@ -0,0 +1,29 @@ +# #219 — Fusion : panneau Spark v2 (Lot 1) + +**Contexte.** Le panneau Spark livré en 2.74.0 (#218) affichait les +réponses de l'agent en texte brut (les `##` et `**` du Markdown +restaient visibles), avait une largeur fixe, un thème clair codé en +dur déconnecté de l'application hôte, et une seule conversation sans +gestion. Le Lot 1 du « Panneau v2 » (carte fusion-trois-apps #17, +arbitrage de Bruno du 2026-10-22) corrige ces quatre points, à +l'identique dans les quatre applications du programme. + +**Livraison (côté ObsiGate : assets vendangés uniquement).** + +- `frontend/draco-panneau.js` / `frontend/draco-panneau.css` + remplacés par la v2 partagée (octet pour octet avec TikTak, + Flowdeck et Kanbante) : + - **Markdown rendu** : les réponses et les rapports de missions + affichent le HTML produit par le service d'agents (0.10.0 : + mistune + sanitizer côté serveur) ; + - **largeur réglable** à la souris (poignée, persistée, + double-clic = largeur par défaut) ; + - **thème clair/sombre aligné** sur l'application hôte + (`html[data-theme]`, ici celui d'ObsiGate, suivi en direct) ; + - **discussions façon Muse** : discussion principale + perpétuelle + discussions parallèles créées, archivées et + supprimées depuis le tiroir du panneau. +- Le travail de fond est dans le service d'agents (agent-commun + 0.10.0) ; ObsiGate ne change aucun code propre. Le `/static` est + déjà servi en `Cache-Control: no-cache` : aucun versionnage + d'asset à ajouter. Redéploiement Docker requis. diff --git a/frontend/draco-panneau.css b/frontend/draco-panneau.css index 743bc07..d337013 100644 --- a/frontend/draco-panneau.css +++ b/frontend/draco-panneau.css @@ -1,83 +1,189 @@ -/* Panneau Spark — branchement au service d'agents (Fusion, jalon J1). - Composant d'intégration TikTak : le panneau définitif sera vendangé - depuis dracodev-commons ; celui-ci prouve le chemin panneau → service. */ +/* Panneau Spark — service d'agents (Fusion, J1 → Panneau v2 Lot 1). + Thème clair/sombre piloté par [data-draco-theme] sur le panneau : + le JS le cale sur le thème de l'app hôte (html[data-theme], repli + prefers-color-scheme) et suit les bascules en direct. Toutes les + couleurs passent par les variables ci-dessous — en-tête et fenêtre + partagent donc toujours le même mode. */ .draco-panneau { - position: fixed; top: 0; right: 0; height: 100dvh; width: 388px; max-width: 94vw; - background: #fbfbfa; color: #242624; z-index: 400; + --p-fond: #fbfbfa; --p-surface: #ffffff; --p-bord: #e4e5e0; + --p-texte: #242624; --p-doux: #6c7268; --p-faint: #8a8f84; + --p-chip: #f4f5f1; --p-chip-bord: #d6d8d1; --p-chip-survol: #e9ebe4; + --p-agent: #eef0ea; --p-champ: #ffffff; --p-code: #e8eae3; + --p-appro-fond: #fdf6ec; --p-danger-fond: #f6e3de; --p-danger: #a03a28; + --p-accent: #7c5cd6; --p-lien: #5b3df0; + position: fixed; top: 0; right: 0; height: 100dvh; width: 388px; max-width: 96vw; + background: var(--p-fond); color: var(--p-texte); z-index: 400; box-shadow: -12px 0 36px rgb(0 0 0 / .22); display: flex; flex-direction: column; transform: translateX(105%); transition: transform .28s ease; font: 14px/1.5 system-ui, "Segoe UI", sans-serif; } +.draco-panneau[data-draco-theme="dark"] { + --p-fond: #17181a; --p-surface: #1f2023; --p-bord: #33353a; + --p-texte: #eceeeb; --p-doux: #a6aba0; --p-faint: #83887e; + --p-chip: #26272b; --p-chip-bord: #3a3c41; --p-chip-survol: #2e3036; + --p-agent: #26272b; --p-champ: #1f2023; --p-code: #2c2e33; + --p-appro-fond: #2a2118; --p-danger-fond: #40241f; --p-danger: #e8a08d; + --p-lien: #b39df5; +} .draco-panneau.open { transform: translateX(0); } +.draco-poignee { + position: absolute; left: 0; top: 0; bottom: 0; width: 8px; + cursor: ew-resize; z-index: 5; touch-action: none; +} +.draco-poignee:hover { background: color-mix(in srgb, var(--p-accent) 35%, transparent); } +@media (max-width: 700px) { .draco-poignee { display: none; } } .draco-panneau-hd { display: flex; align-items: center; gap: 9px; - padding: 13px 15px; border-bottom: 1px solid #e4e5e0; background: #fff; + padding: 13px 15px; border-bottom: 1px solid var(--p-bord); background: var(--p-surface); } .draco-panneau-hd .titre { font-weight: 700; font-size: 15px; } -.draco-panneau-hd .soustitre { font-size: 12px; color: #6c7268; } +.draco-panneau-hd .soustitre { font-size: 12px; color: var(--p-doux); } .draco-panneau-hd .fermer { - margin-left: auto; border: 0; background: #eef0ea; border-radius: 8px; - width: 30px; height: 30px; cursor: pointer; font-size: 14px; + margin-left: auto; border: 0; background: var(--p-agent); color: var(--p-texte); + border-radius: 8px; width: 30px; height: 30px; cursor: pointer; font-size: 14px; } .draco-panneau-etat { display: flex; align-items: center; gap: 7px; font-size: 12.5px; } .draco-panneau-etat .point { width: 9px; height: 9px; border-radius: 50%; background: #b9bdb2; flex: none; } .draco-panneau-etat.ok .point { background: #2ea36b; } .draco-panneau-etat.err .point { background: #cf4b32; } -.draco-panneau-outils { display: flex; gap: 8px; padding: 9px 15px; border-bottom: 1px solid #e4e5e0; background: #fff; } -.draco-panneau-outils button { - border: 1px solid #d6d8d1; background: #f4f5f1; border-radius: 999px; - padding: 5px 12px; font: 600 12.5px system-ui; cursor: pointer; +/* Barre des discussions (façon Muse) */ +.draco-disc-bar { + display: flex; align-items: center; gap: 7px; + padding: 8px 15px; border-bottom: 1px solid var(--p-bord); background: var(--p-surface); } -.draco-panneau-outils button:hover { background: #e9ebe4; } +.draco-disc-bar .d-titre { + flex: 1; font-weight: 600; font-size: 13px; min-width: 0; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.draco-disc-bar .d-btn { + border: 1px solid var(--p-chip-bord); background: var(--p-chip); color: var(--p-texte); + border-radius: 999px; padding: 4px 10px; font: 600 12px system-ui; cursor: pointer; flex: none; +} +.draco-disc-bar .d-btn:hover { background: var(--p-chip-survol); } +.draco-discussions { + border-bottom: 1px solid var(--p-bord); background: var(--p-surface); + max-height: 46%; overflow-y: auto; padding: 8px 12px; +} +.draco-discussions .d-section { + font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: .5px; color: var(--p-doux); margin: 8px 2px 4px; +} +.draco-discussions .d-section:first-child { margin-top: 0; } +.draco-discussions .d-item { + border: 1px solid transparent; border-radius: 9px; padding: 6px 8px; cursor: pointer; +} +.draco-discussions .d-item:hover { background: var(--p-chip); } +.draco-discussions .d-item.actif { border-color: var(--p-accent); background: var(--p-chip); } +.draco-discussions .d-meta { font-weight: 600; font-size: 13px; } +.draco-discussions .d-sous { font-size: 12px; color: var(--p-doux); } +.draco-discussions .d-actions { display: flex; gap: 6px; margin-top: 5px; } +.draco-discussions .d-actions button { + border: 1px solid var(--p-chip-bord); background: var(--p-surface); color: var(--p-texte); + border-radius: 7px; padding: 3px 9px; font: 600 11.5px system-ui; cursor: pointer; +} +.draco-discussions .d-actions button.danger { color: var(--p-danger); border-color: var(--p-danger-fond); } +.draco-discussions .d-vide, .draco-discussions .d-chargement { + font-size: 12.5px; color: var(--p-doux); padding: 2px 8px 6px; +} +.draco-panneau-outils { display: flex; gap: 8px; padding: 9px 15px; border-bottom: 1px solid var(--p-bord); background: var(--p-surface); } +.draco-panneau-outils button { + border: 1px solid var(--p-chip-bord); background: var(--p-chip); color: var(--p-texte); + border-radius: 999px; padding: 5px 12px; font: 600 12.5px system-ui; cursor: pointer; +} +.draco-panneau-outils button:hover { background: var(--p-chip-survol); } .draco-appro { - align-self: stretch; background: #fdf6ec; border: 1.6px solid #e8a13c; + align-self: stretch; background: var(--p-appro-fond); border: 1.6px solid #e8a13c; border-radius: 12px; padding: 10px 12px; } .draco-appro .titre { font-weight: 700; font-size: 12px; color: #9a6a1c; text-transform: uppercase; letter-spacing: .4px; } +.draco-panneau[data-draco-theme="dark"] .draco-appro .titre { color: #e8bc7a; } .draco-appro .resume { margin: 5px 0 8px; } .draco-appro .btns { display: flex; gap: 7px; } -.draco-appro .btns button { border: 0; border-radius: 8px; padding: 6px 11px; font: 600 12.5px system-ui; cursor: pointer; background: #eef0ea; } +.draco-appro .btns button { border: 0; border-radius: 8px; padding: 6px 11px; font: 600 12.5px system-ui; cursor: pointer; background: var(--p-agent); color: var(--p-texte); } .draco-appro .btns button.go { background: #2ea36b; color: #fff; } -.draco-auj { align-self: stretch; background: #fff; border: 1px solid #e4e5e0; border-radius: 12px; padding: 10px 12px; } -.draco-auj h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: #6c7268; margin: 6px 0 4px; } +.draco-auj { align-self: stretch; background: var(--p-surface); border: 1px solid var(--p-bord); border-radius: 12px; padding: 10px 12px; } +.draco-auj h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: var(--p-doux); margin: 6px 0 4px; } .draco-auj h5:first-child { margin-top: 0; } .draco-auj ul { margin: 0; padding-left: 17px; } -.draco-auj .etat-source { color: #6c7268; font-size: 12.5px; } +.draco-auj .etat-source { color: var(--p-doux); font-size: 12.5px; } .draco-panneau-msgs { flex: 1; overflow-y: auto; padding: 14px 15px; display: flex; flex-direction: column; gap: 9px; } .draco-msg { max-width: 88%; padding: 9px 12px; border-radius: 13px; white-space: pre-wrap; word-wrap: break-word; } .draco-msg.user { align-self: flex-end; background: #2f6fed; color: #fff; border-bottom-right-radius: 4px; } -.draco-msg.agent { align-self: flex-start; background: #eef0ea; border-bottom-left-radius: 4px; } -.draco-msg.sys { align-self: center; background: none; color: #6c7268; font-size: 12.5px; text-align: center; max-width: 96%; } -.draco-panneau-ft { border-top: 1px solid #e4e5e0; padding: 11px 12px; background: #fff; } +.draco-msg.agent { align-self: flex-start; background: var(--p-agent); border-bottom-left-radius: 4px; } +.draco-msg.sys { align-self: center; background: none; color: var(--p-doux); font-size: 12.5px; text-align: center; max-width: 96%; } +.draco-panneau-ft { border-top: 1px solid var(--p-bord); padding: 11px 12px; background: var(--p-surface); } .draco-panneau-form { display: flex; gap: 8px; } .draco-panneau-form input { - flex: 1; border: 1px solid #d6d8d1; border-radius: 10px; padding: 9px 11px; font: inherit; + flex: 1; border: 1px solid var(--p-chip-bord); border-radius: 10px; padding: 9px 11px; + font: inherit; background: var(--p-champ); color: var(--p-texte); } .draco-panneau-form button { - border: 0; border-radius: 10px; background: #7c5cd6; color: #fff; + border: 0; border-radius: 10px; background: var(--p-accent); color: #fff; padding: 9px 14px; font: inherit; font-weight: 600; cursor: pointer; } .draco-panneau-form button:disabled { opacity: .5; cursor: default; } -.draco-panneau-note { font-size: 11.5px; color: #8a8f84; margin-top: 8px; text-align: center; } -.draco-missions { align-self: stretch; background: #fff; border: 1px solid #e4e5e0; border-radius: 12px; padding: 10px 12px; } -.draco-missions h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: #6c7268; margin: 0 0 6px; } -.draco-missions label { display: block; font-size: 12px; font-weight: 600; color: #6c7268; margin: 8px 0 3px; } +.draco-panneau-note { font-size: 11.5px; color: var(--p-faint); margin-top: 8px; text-align: center; } +.draco-missions { align-self: stretch; background: var(--p-surface); border: 1px solid var(--p-bord); border-radius: 12px; padding: 10px 12px; } +.draco-missions h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: var(--p-doux); margin: 0 0 6px; } +.draco-missions label { display: block; font-size: 12px; font-weight: 600; color: var(--p-doux); margin: 8px 0 3px; } .draco-missions select, .draco-missions input, .draco-missions textarea { - width: 100%; box-sizing: border-box; border: 1px solid #d6d8d1; border-radius: 8px; - padding: 7px 9px; font: inherit; font-size: 13px; background: #fff; + width: 100%; box-sizing: border-box; border: 1px solid var(--p-chip-bord); border-radius: 8px; + padding: 7px 9px; font: inherit; font-size: 13px; background: var(--p-champ); color: var(--p-texte); } .draco-missions .btns { display: flex; gap: 7px; margin-top: 10px; } -.draco-missions .btns button { border: 0; border-radius: 8px; padding: 6px 11px; font: 600 12.5px system-ui; cursor: pointer; background: #eef0ea; } -.draco-missions .btns button.go { background: #7c5cd6; color: #fff; } +.draco-missions .btns button { border: 0; border-radius: 8px; padding: 6px 11px; font: 600 12.5px system-ui; cursor: pointer; background: var(--p-agent); color: var(--p-texte); } +.draco-missions .btns button.go { background: var(--p-accent); color: #fff; } .draco-missions .m-liste { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; } -.draco-missions .etat-source { color: #6c7268; font-size: 12.5px; } -.draco-missions .m-item { border: 1px solid #e4e5e0; border-radius: 10px; padding: 8px 10px; } +.draco-missions .etat-source { color: var(--p-doux); font-size: 12.5px; } +.draco-missions .m-item { border: 1px solid var(--p-bord); border-radius: 10px; padding: 8px 10px; } .draco-missions .m-entete { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; } -.draco-missions .m-statut { font-size: 11.5px; font-weight: 700; color: #6c7268; white-space: nowrap; } +.draco-missions .m-statut { font-size: 11.5px; font-weight: 700; color: var(--p-doux); white-space: nowrap; } .draco-missions .m-statut.st-succeeded { color: #2ea36b; } .draco-missions .m-statut.st-failed { color: #cf4b32; } .draco-missions .m-statut.st-running, .draco-missions .m-statut.st-paused_question { color: #9a6a1c; } -.draco-missions .m-meta { font-size: 12px; color: #6c7268; } +.draco-missions .m-meta { font-size: 12px; color: var(--p-doux); } .draco-missions .m-rapport { font-size: 12.5px; margin-top: 5px; white-space: pre-wrap; word-wrap: break-word; } -.draco-missions .m-annuler { margin-top: 6px; border: 0; border-radius: 7px; padding: 4px 9px; font: 600 12px system-ui; cursor: pointer; background: #f6e3de; color: #a03a28; } +.draco-missions .m-annuler { margin-top: 6px; border: 0; border-radius: 7px; padding: 4px 9px; font: 600 12px system-ui; cursor: pointer; background: var(--p-danger-fond); color: var(--p-danger); } +/* ── Rendu Markdown (HTML produit par le service) ──────────── */ +.draco-msg.md, .draco-missions .m-rapport.md { white-space: normal; } +.draco-msg.md > *:first-child, .m-rapport.md > *:first-child { margin-top: 0; } +.draco-msg.md > *:last-child, .m-rapport.md > *:last-child { margin-bottom: 0; } +.draco-msg.md p, .m-rapport.md p { margin: 6px 0; } +.draco-msg.md h1, .draco-msg.md h2, .draco-msg.md h3, +.draco-msg.md h4, .draco-msg.md h5, .draco-msg.md h6 { margin: 10px 0 4px; line-height: 1.3; } +.draco-msg.md h1 { font-size: 16.5px; } .draco-msg.md h2 { font-size: 15.5px; } +.draco-msg.md h3 { font-size: 14.5px; } .draco-msg.md h4, +.draco-msg.md h5, .draco-msg.md h6 { font-size: 13.5px; } +.draco-msg.md ul, .draco-msg.md ol, .m-rapport.md ul, .m-rapport.md ol { margin: 6px 0; padding-left: 20px; } +.draco-msg.md li, .m-rapport.md li { margin: 2px 0; } +.draco-msg.md code, .m-rapport.md code { + background: var(--p-code); border-radius: 5px; padding: 1px 5px; + font: 12.5px ui-monospace, "Cascadia Mono", Consolas, monospace; +} +.draco-msg.md pre, .m-rapport.md pre { + background: var(--p-code); border-radius: 9px; padding: 9px 11px; + overflow-x: auto; margin: 7px 0; +} +.draco-msg.md pre code, .m-rapport.md pre code { background: none; padding: 0; } +.draco-msg.md blockquote, .m-rapport.md blockquote { + border-left: 3px solid var(--p-accent); margin: 7px 0; padding: 2px 0 2px 11px; + color: var(--p-doux); +} +.draco-msg.md blockquote.callout, .m-rapport.md blockquote.callout { + border-radius: 0 8px 8px 0; padding: 6px 11px; color: var(--p-texte); + background: var(--p-chip); +} +.draco-msg.md blockquote.callout-warning { border-left-color: #e8a13c; } +.draco-msg.md blockquote.callout-danger { border-left-color: #cf4b32; } +.draco-msg.md blockquote.callout-tip { border-left-color: #2ea36b; } +.draco-msg.md a, .m-rapport.md a { color: var(--p-lien); } +.draco-msg.md table, .m-rapport.md table { + border-collapse: collapse; margin: 7px 0; font-size: 12.5px; + display: block; overflow-x: auto; max-width: 100%; +} +.draco-msg.md th, .draco-msg.md td, .m-rapport.md th, .m-rapport.md td { + border: 1px solid var(--p-bord); padding: 4px 8px; text-align: left; +} +.draco-msg.md hr, .m-rapport.md hr { border: 0; border-top: 1px solid var(--p-bord); margin: 9px 0; } +.draco-msg.md input[type="checkbox"], .m-rapport.md input[type="checkbox"] { margin-right: 5px; } diff --git a/frontend/draco-panneau.js b/frontend/draco-panneau.js index 1eeeb55..5a0d8ec 100644 --- a/frontend/draco-panneau.js +++ b/frontend/draco-panneau.js @@ -1,43 +1,129 @@ -/* Panneau Spark — branchement au service d'agents (Fusion, J1 → J5). +/* Panneau Spark — branchement au service d'agents (Fusion, J1 → Panneau v2). * * Parle à l'API du service d'agents (contrat de dracodev-commons) : * GET /api/v1/me — identité (cookie draco_session) * POST /api/v1/chat — conversation réelle du noyau (J2) : - * LLM DeepSeek + outils MCP des apps - * GET /api/v1/conversations/… — historique de la conversation - * GET /api/v1/executors — registre (noyau, sous-agents J3, + * LLM DeepSeek + outils MCP des apps ; + * la réponse arrive avec son HTML + * Markdown rendu côté service (v2) + * GET /api/v1/conversations — discussions façon Muse (v2) : une + * principale perpétuelle + des + * parallèles créées/archivées/ + * supprimées par l'utilisateur + * GET /api/v1/conversations/… — historique d'une discussion (les + * messages agent portent leur html) + * GET /api/v1/executors — registre (noyau, sous-agents J3, * externes J4 : état de configuration) - * POST /api/v1/missions — missions longues (J5) : lancement, - * suivi, rapport, annulation + * POST /api/v1/missions — missions longues (J5) + * Panneau v2 (Lot 1) : rendu Markdown, largeur réglable à la souris + * (persistée), thème clair/sombre aligné sur l'app hôte + * (html[data-theme], repli prefers-color-scheme, suivi en direct). * États honnêtes : service injoignable / session non reconnue / LLM non * configuré / exécuteur non configuré sont affichés tels quels. */ (function () { "use strict"; var API = "https://agent.dracodev.net"; var convId = null; - try { convId = sessionStorage.getItem("draco_panneau_conv") || null; } catch (e) { /* stockage indispo */ } + var convKind = null; // 'principale' | 'parallele' + var convTitre = ""; var etatConnu = false; - var historiqueCharge = false; var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null; + var titreEl = null, tiroirEl = null; function domaine() { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } } + function stockerConv() { + try { + if (convId) sessionStorage.setItem("draco_panneau_conv", convId); + else sessionStorage.removeItem("draco_panneau_conv"); + } catch (e) { /* stockage indisponible */ } + } + function convStockee() { + try { return sessionStorage.getItem("draco_panneau_conv") || null; } + catch (e) { return null; } + } + + /* ── Thème : suivre l'app hôte ─────────────────────────────── */ + function themeHote() { + var t = document.documentElement.getAttribute("data-theme"); + if (t === "dark" || t === "light") return t; + if (document.documentElement.classList.contains("dark") || + document.body.classList.contains("dark")) return "dark"; + try { + if (window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark"; + } catch (e) { /* pas de matchMedia */ } + return "light"; + } + function appliquerTheme() { + if (root) root.setAttribute("data-draco-theme", themeHote()); + } + function surveillerTheme() { + try { + new MutationObserver(appliquerTheme).observe(document.documentElement, + { attributes: true, attributeFilter: ["data-theme", "class"] }); + window.matchMedia("(prefers-color-scheme: dark)") + .addEventListener("change", appliquerTheme); + } catch (e) { /* surveillance facultative */ } + } + + /* ── Largeur réglable ──────────────────────────────────────── */ + function largeurStockee() { + try { + var v = parseInt(localStorage.getItem("draco_panneau_largeur") || "", 10); + return isNaN(v) ? null : v; + } catch (e) { return null; } + } + function appliquerLargeur() { + if (!root) return; + var v = largeurStockee(); + root.style.width = v ? Math.min(v, Math.round(window.innerWidth * 0.96)) + "px" : ""; + } + function brancherPoignee(poignee) { + poignee.addEventListener("pointerdown", function (ev) { + if (window.innerWidth <= 700) return; // mobile : pleine largeur + ev.preventDefault(); + var x0 = ev.clientX, w0 = root.getBoundingClientRect().width; + function bouger(e2) { + var w = Math.max(300, Math.min(760, w0 + (x0 - e2.clientX))); + root.style.width = w + "px"; + } + function lacher(e2) { + document.removeEventListener("pointermove", bouger); + document.removeEventListener("pointerup", lacher); + var w = Math.round(root.getBoundingClientRect().width); + try { localStorage.setItem("draco_panneau_largeur", String(w)); } catch (e) { /* ok */ } + } + document.addEventListener("pointermove", bouger); + document.addEventListener("pointerup", lacher); + }); + poignee.addEventListener("dblclick", function () { + try { localStorage.removeItem("draco_panneau_largeur"); } catch (e) { /* ok */ } + appliquerLargeur(); + }); + } function construire() { root = document.createElement("aside"); root.className = "draco-panneau"; root.setAttribute("aria-label", "Spark, l'agent commun"); root.innerHTML = + '
' + '
' + ' ✦' + '
Spark — agent commun
' + '
Service d\'agents dracodev
' + ' ' + '
' + - '
' + + '
' + ' Connexion au service…' + '
' + + '
' + + ' Discussion principale' + + ' ' + + ' ' + + '
' + + '' + '
' + ' ' + ' ' + @@ -54,15 +140,23 @@ etatEl = root.querySelector(".draco-panneau-etat"); inputEl = root.querySelector("input"); sendBtn = root.querySelector("button[type=submit]"); + titreEl = root.querySelector(".d-titre"); + tiroirEl = root.querySelector(".draco-discussions"); root.querySelector(".fermer").addEventListener("click", fermer); + root.querySelector('[data-disc="liste"]').addEventListener("click", basculerTiroir); + root.querySelector('[data-disc="nouvelle"]').addEventListener("click", nouvelleDiscussion); root.querySelector('[data-outil="aujourdhui"]').addEventListener("click", chargerAujourdhui); root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations); root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions); root.querySelector("form").addEventListener("submit", envoyer); + brancherPoignee(root.querySelector(".draco-poignee")); + appliquerTheme(); + surveillerTheme(); + appliquerLargeur(); + window.addEventListener("resize", appliquerLargeur); document.addEventListener("keydown", function (ev) { if (ev.key === "Escape" && root.classList.contains("open")) fermer(); }); - ajouterMsg("sys", "Conversation avec le service d'agents — domaine : " + domaine() + "."); } function ajouterMsg(kind, texte) { @@ -74,10 +168,26 @@ return d; } + /* Message agent rendu en Markdown : le HTML vient du service + * (rendu + sanitizer côté serveur) ; repli texte si absent. */ + function ajouterMsgMd(texte, html) { + var d = document.createElement("div"); + d.className = "draco-msg agent md"; + if (html) d.innerHTML = html; + else d.textContent = texte; + msgsEl.appendChild(d); + msgsEl.scrollTop = msgsEl.scrollHeight; + return d; + } + function majEtat(cls, texte) { etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls; etatEl.querySelector(".txt").textContent = texte; } + function majTitre() { + titreEl.textContent = convTitre || "Discussion principale"; + titreEl.title = titreEl.textContent; + } function verifierIdentite() { if (etatConnu) return; @@ -109,6 +219,200 @@ }); } + /* ── Discussions (façon Muse) ────────────────────────────── */ + function chargerListeDiscussions() { + return fetch(API + "/api/v1/conversations?domain=" + encodeURIComponent(domaine()), + { credentials: "include" }) + .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }); + } + + function demarrerDiscussions() { + chargerListeDiscussions() + .then(function (d) { + var precedente = convStockee(); + var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []); + var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0]; + var active = trouvee || d.principale; + if (!active) return; + convId = active.id; convKind = active.kind; convTitre = active.title; + stockerConv(); majTitre(); + chargerHistorique(); + }) + .catch(function () { + ajouterMsg("sys", "Discussions : service injoignable pour le moment."); + }); + } + + function basculerDiscussion(c) { + convId = c.id; convKind = c.kind; convTitre = c.title; + stockerConv(); majTitre(); + tiroirEl.hidden = true; + msgsEl.innerHTML = ""; + ajouterMsg("sys", (c.kind === "principale" + ? "Discussion principale — le fil sans fin." + : "Discussion parallèle : " + c.title) + " · domaine : " + domaine() + "."); + chargerHistorique(); + inputEl.focus(); + } + + function nouvelleDiscussion() { + fetch(API + "/api/v1/conversations", { + method: "POST", credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ domain: domaine() }) + }) + .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) + .then(function (c) { + basculerDiscussion(c); + ajouterMsg("sys", "Nouvelle discussion parallèle créée — elle prendra le titre de ton premier message."); + }) + .catch(function () { ajouterMsg("sys", "Création impossible (service injoignable ?)."); }); + } + + function actionDiscussion(id, action) { + var methode = action === "supprimer" ? "DELETE" : "POST"; + var url = API + "/api/v1/conversations/" + id + (action === "supprimer" ? "" : "/" + action); + return fetch(url, { method: methode, credentials: "include" }) + .then(function (r) { + if (!r.ok) return r.json().catch(function () { return null; }).then(function (e) { + throw new Error(e && e.detail ? e.detail : "HTTP " + r.status); + }); + return r.json(); + }); + } + + function ligneDiscussion(c, actions) { + var ligne = document.createElement("div"); + ligne.className = "d-item" + (c.id === convId ? " actif" : ""); + var meta = document.createElement("div"); + meta.className = "d-meta"; + var quand = ""; + try { + quand = new Date(c.updated_at * 1000).toLocaleDateString("fr-CA", + { day: "numeric", month: "short" }); + } catch (e) { /* date illisible */ } + meta.textContent = (c.kind === "principale" ? "📌 " : "") + c.title; + ligne.appendChild(meta); + var sous = document.createElement("div"); + sous.className = "d-sous"; + sous.textContent = c.nb_messages + " message(s)" + (quand ? " · " + quand : "") + + (c.kind === "principale" ? " · perpétuelle" : ""); + ligne.appendChild(sous); + meta.addEventListener("click", function () { basculerDiscussion(c); }); + sous.addEventListener("click", function () { basculerDiscussion(c); }); + if (actions && actions.length) { + var btns = document.createElement("div"); + btns.className = "d-actions"; + actions.forEach(function (a) { + var b = document.createElement("button"); + b.type = "button"; b.textContent = a.label; + if (a.danger) b.className = "danger"; + b.addEventListener("click", function (ev) { + ev.stopPropagation(); + if (a.confirmer && !window.confirm(a.confirmer)) return; + actionDiscussion(c.id, a.action) + .then(function () { + if (c.id === convId && a.action !== "desarchiver") { + chargerListeDiscussions().then(function (d) { + if (d.principale) { + basculerDiscussion(d.principale); + ajouterMsg("sys", a.action === "supprimer" + ? "Discussion supprimée — retour à la discussion principale." + : "Discussion archivée — retour à la discussion principale."); + } + }); + } + remplirTiroir(); + }) + .catch(function (e) { ajouterMsg("sys", "Action refusée : " + e.message); }); + }); + btns.appendChild(b); + }); + ligne.appendChild(btns); + } + return ligne; + } + + function remplirTiroir() { + tiroirEl.innerHTML = "
Chargement des discussions…
"; + chargerListeDiscussions() + .then(function (d) { + tiroirEl.innerHTML = ""; + var h = document.createElement("div"); + h.className = "d-section"; + h.textContent = "Discussion principale"; + tiroirEl.appendChild(h); + if (d.principale) tiroirEl.appendChild(ligneDiscussion(d.principale, [])); + h = document.createElement("div"); + h.className = "d-section"; + h.textContent = "Discussions parallèles"; + tiroirEl.appendChild(h); + if (!d.paralleles.length) { + var vide = document.createElement("div"); + vide.className = "d-vide"; + vide.textContent = "Aucune pour le moment — bouton « + Nouvelle »."; + tiroirEl.appendChild(vide); + } + d.paralleles.forEach(function (c) { + tiroirEl.appendChild(ligneDiscussion(c, [ + { label: "Archiver", action: "archive" }, + { label: "Supprimer", action: "supprimer", danger: true, + confirmer: "Supprimer définitivement la discussion « " + c.title + " » ?" } + ])); + }); + if (d.archivees && d.archivees.length) { + h = document.createElement("div"); + h.className = "d-section"; + h.textContent = "Archivées"; + tiroirEl.appendChild(h); + d.archivees.forEach(function (c) { + tiroirEl.appendChild(ligneDiscussion(c, [ + { label: "Désarchiver", action: "desarchiver" }, + { label: "Supprimer", action: "supprimer", danger: true, + confirmer: "Supprimer définitivement la discussion « " + c.title + " » ?" } + ])); + }); + } + }) + .catch(function () { + tiroirEl.innerHTML = "
Discussions : service injoignable.
"; + }); + } + + function basculerTiroir() { + tiroirEl.hidden = !tiroirEl.hidden; + if (!tiroirEl.hidden) remplirTiroir(); + } + + function chargerHistorique() { + if (!convId) return; + fetch(API + "/api/v1/conversations/" + convId, { credentials: "include" }) + .then(function (r) { + if (r.status === 404) { convId = null; stockerConv(); return null; } + if (!r.ok) return null; + return r.json(); + }) + .then(function (d) { + if (!d) return; + convKind = d.kind; convTitre = d.title || convTitre; + majTitre(); + if (!msgsEl.querySelector(".draco-msg")) { + ajouterMsg("sys", (d.kind === "principale" + ? "Discussion principale — le fil sans fin." + : "Discussion parallèle : " + (d.title || "")) + " · domaine : " + domaine() + "."); + } + var sys = msgsEl.querySelector(".draco-msg.sys"); + (d.messages || []).forEach(function (m) { + var el = m.role === "user" + ? ajouterMsg("user", m.content) + : ajouterMsgMd(m.content, m.html); + if (sys) { msgsEl.insertBefore(el, sys.nextSibling); sys = el; } + }); + msgsEl.scrollTop = msgsEl.scrollHeight; + }) + .catch(function () { /* historique indisponible : on repart à neuf */ }); + } + function chargerAujourdhui() { var bloc = document.createElement("div"); bloc.className = "draco-auj"; @@ -178,7 +482,7 @@ .then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(function (d) { bloc.style.opacity = ".55"; - bloc.querySelector(".btns").innerHTML = "Décision enregistrée : " + esc(d.status) + ""; + bloc.querySelector(".btns").innerHTML = "Décision enregistrée : " + esc(d.status) + ""; }) .catch(function () { ajouterMsg("sys", "La décision n'a pas pu être envoyée (service injoignable ?)."); }); } @@ -239,12 +543,24 @@ d.missions.slice(0, 10).forEach(function (m) { var ligne = document.createElement("div"); ligne.className = "m-item"; - var rapport = m.result ? "
" + esc(String(m.result).slice(0, 700)) + "
" : ""; - ligne.innerHTML = "
" + esc(m.title) + "" + - "" + esc(STATUTS_MISSION[m.status] || m.status) + "
" + - "
" + esc(m.executor) + - (m.meta && m.meta.duree_s ? " · " + esc(m.meta.duree_s) + " s" : "") + - (m.meta && m.meta.cout ? " · coût : " + esc(m.meta.cout) : "") + "
" + rapport; + var entete = document.createElement("div"); + entete.className = "m-entete"; + entete.innerHTML = "" + esc(m.title) + "" + + "" + esc(STATUTS_MISSION[m.status] || m.status) + ""; + ligne.appendChild(entete); + var meta = document.createElement("div"); + meta.className = "m-meta"; + meta.textContent = m.executor + + (m.meta && m.meta.duree_s ? " · " + m.meta.duree_s + " s" : "") + + (m.meta && m.meta.cout ? " · coût : " + m.meta.cout : ""); + ligne.appendChild(meta); + if (m.result) { + var rapport = document.createElement("div"); + rapport.className = "m-rapport md"; + if (m.result_html) rapport.innerHTML = m.result_html; + else rapport.textContent = String(m.result).slice(0, 1200); + ligne.appendChild(rapport); + } if (m.status === "queued" || m.status === "running" || m.status === "paused_question") { var btn = document.createElement("button"); btn.type = "button"; btn.className = "m-annuler"; btn.textContent = "Annuler"; @@ -290,31 +606,6 @@ .catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); }); } - function chargerHistorique() { - if (historiqueCharge || !convId) return; - historiqueCharge = true; - fetch(API + "/api/v1/conversations/" + convId, { credentials: "include" }) - .then(function (r) { - if (r.status === 404) { - convId = null; - try { sessionStorage.removeItem("draco_panneau_conv"); } catch (e) { /* ok */ } - return null; - } - if (!r.ok) return null; - return r.json(); - }) - .then(function (d) { - if (!d || !d.messages || !d.messages.length) return; - var sys = msgsEl.querySelector(".draco-msg.sys"); - d.messages.forEach(function (m) { - var el = ajouterMsg(m.role === "user" ? "user" : "agent", m.content); - if (sys) msgsEl.insertBefore(el, sys.nextSibling), sys = el; - }); - msgsEl.scrollTop = msgsEl.scrollHeight; - }) - .catch(function () { /* historique indisponible : la conversation repart à neuf */ }); - } - function envoyer(ev) { ev.preventDefault(); var texte = inputEl.value.trim(); @@ -347,8 +638,16 @@ .then(function (d) { if (!d) return; convId = d.conversation_id; - try { sessionStorage.setItem("draco_panneau_conv", convId); } catch (e) { /* ok */ } - attente.textContent = d.reply; + convKind = d.kind || convKind; + if (d.title) convTitre = d.title; + stockerConv(); majTitre(); + if (d.html) { + attente.className = "draco-msg agent md"; + attente.innerHTML = d.html; + } else { + attente.textContent = d.reply; + } + msgsEl.scrollTop = msgsEl.scrollHeight; if (d.tools_used && d.tools_used.length) { ajouterMsg("sys", "Outils utilisés : " + d.tools_used.join(", ") + " · modèle : " + (d.model || "?")); } @@ -362,10 +661,10 @@ } function ouvrir() { - if (!root) construire(); + if (!root) { construire(); demarrerDiscussions(); } root.classList.add("open"); + appliquerTheme(); verifierIdentite(); - chargerHistorique(); setTimeout(function () { inputEl.focus(); }, 300); } function fermer() { if (root) root.classList.remove("open"); } diff --git a/package.json b/package.json index 48795aa..94e44aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.74.0", + "version": "2.75.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.", "main": "patch.js", "directories": {