Compare commits

..
Author SHA1 Message Date
bruno dcec510451 fix(sidebar): CSS composer console, statut dans le titre, pied épuré
CI / lint (pull_request) Failing after 1s
CI / security (pull_request) Failing after 0s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-28 08:49:05 -04:00
bruno cb468b0f3c fix(sidebar): style console, statut dans le titre, composer console, synchro domaine 2026-10-28 08:48:54 -04:00
11 changed files with 434 additions and 57 deletions
+1 -5
View File
@@ -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.81.1**.
> [Unreleased](#unreleased). La dernière version livrée est **2.81.0**.
---
@@ -14,10 +14,6 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.81.1] — 2026-10-28
---
## [2.81.0] — 2026-10-28
---
+3 -3
View File
@@ -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.81.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.81.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.81.1).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.81.0).
---
*Projet : ObsiGate | Version : 2.81.1 | Dernière mise à jour : Septembre 2026*
*Projet : ObsiGate | Version : 2.81.0 | Dernière mise à jour : Septembre 2026*
+3 -3
View File
@@ -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.81.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.81.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.81.1).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.81.0).
---
*Project: ObsiGate | Version: 2.81.1 | Last updated: September 2026*
*Project: ObsiGate | Version: 2.81.0 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.81.1
2.81.0
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.81.1"
version = "2.81.0"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.81.1"
version = "2.81.0"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.81.1",
"version": "2.81.0",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.81.1 | **Dernière mise à jour :** 2026-10-28
> **Version :** 2.81.0 | **Dernière mise à jour :** 2026-10-28
> **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)**
+89 -17
View File
@@ -37,16 +37,19 @@
display: flex; align-items: center; gap: 9px;
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: var(--p-doux); }
.draco-panneau-titres { min-width: 0; flex: 1; }
.draco-panneau-hd .titre { font-weight: 700; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.draco-panneau-hd .soustitre {
font-size: 12px; color: var(--p-doux); display: flex; align-items: center; gap: 6px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.draco-panneau-hd .soustitre .point { width: 8px; height: 8px; border-radius: 50%; background: #b9bdb2; flex: none; }
.draco-panneau-hd .soustitre.ok .point { background: #2ea36b; }
.draco-panneau-hd .soustitre.err .point { background: #cf4b32; }
.draco-panneau-hd .fermer {
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;
border-radius: 8px; width: 30px; height: 30px; cursor: pointer; font-size: 14px; flex: none;
}
.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; }
/* Barre des discussions (façon Muse) */
.draco-disc-bar {
display: flex; align-items: center; gap: 7px;
@@ -112,18 +115,87 @@
.draco-msg.user { align-self: flex-end; background: #2f6fed; color: #fff; border-bottom-right-radius: 4px; }
.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 var(--p-chip-bord); border-radius: 10px; padding: 9px 11px;
font: inherit; background: var(--p-champ); color: var(--p-texte);
.draco-panneau-ft { padding: 10px 12px 11px; background: var(--p-fond); }
/* Puces de suggestions, comme la console : au-dessus du composer. */
.draco-puces {
display: flex; flex-wrap: wrap; gap: 7px; justify-content: center; padding: 0 2px 8px;
}
.draco-panneau-form button {
border: 0; border-radius: 10px; background: var(--p-accent); color: #fff;
padding: 9px 14px; font: inherit; font-weight: 600; cursor: pointer;
.draco-puces 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-puces button:hover { background: var(--p-chip-survol); }
/* ── Composer façon console (pilule) ─────────────────────────
* +, textarea auto-extensible, micro ou envoi rond bleu ↑ ;
* pendant la dictée, la rangée d'enregistrement remplace la saisie. */
.draco-composer {
display: flex; flex-direction: column; align-items: stretch; gap: 6px;
background: var(--p-surface); border: 1px solid var(--p-bord);
border-radius: 24px; padding: 8px 10px;
box-shadow: 0 1px 4px rgb(0 0 0 / .05);
}
.draco-composer [hidden] { display: none !important; }
.draco-composer-ligne { display: flex; align-items: flex-end; gap: 6px; }
.draco-composer textarea {
flex: 1; border: 0; background: none; resize: none; outline: none;
padding: 8px 2px; max-height: 152px; overflow-y: auto; line-height: 1.45;
min-height: calc(1.45em + 16px); font: inherit; color: var(--p-texte);
scrollbar-width: none;
}
.draco-composer textarea::-webkit-scrollbar { width: 0; height: 0; display: none; }
.draco-composer .composer-btn {
width: 36px; height: 36px; border-radius: 50%; border: 0; flex: none;
background: var(--p-chip); color: var(--p-texte); font-size: 17px;
display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer;
}
.draco-composer .composer-btn:hover { background: var(--p-chip-survol); }
.draco-composer .composer-btn.danger { color: var(--p-danger); }
.draco-composer .composer-envoi {
width: 36px; height: 36px; border-radius: 50%; border: 0; flex: none;
background: #2f6fed; color: #fff; font-size: 17px; font-weight: 700;
display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer;
}
.draco-composer .composer-envoi:disabled { opacity: .5; }
.draco-composer .composer-btn .ic, .draco-composer .composer-envoi .ic { width: 19px; height: 19px; }
.draco-composer-pieces { display: flex; flex-wrap: wrap; gap: 8px; padding: 2px 2px 0; }
.draco-composer-pieces .piece-carte {
position: relative; display: flex; align-items: center; gap: 8px;
background: var(--p-chip); border: 1px solid var(--p-chip-bord);
border-radius: 12px; padding: 5px 9px 5px 5px; max-width: 230px;
}
.draco-composer-pieces .piece-carte img { width: 42px; height: 42px; object-fit: cover; border-radius: 8px; display: block; }
.draco-composer-pieces .piece-nom { font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.draco-composer-pieces .piece-doc-ext {
width: 42px; height: 42px; display: flex; align-items: center; justify-content: center;
background: var(--p-chip); border: 1px solid var(--p-chip-bord); border-radius: 8px;
}
.draco-composer-pieces .piece-ext { font-weight: 700; font-size: 10px; letter-spacing: .04em; text-transform: uppercase; opacity: .75; }
.draco-composer-pieces .piece-retirer {
position: absolute; top: -7px; right: -7px; width: 20px; height: 20px;
border-radius: 50%; border: 0; background: var(--p-texte); color: var(--p-fond);
font-size: 10px; line-height: 1; padding: 0; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.draco-composer-enreg { display: flex; align-items: center; gap: 10px; padding: 0 2px; }
.draco-composer-enreg .enreg-temps { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 36px; }
.draco-composer-enreg .enreg-texte {
flex: 1; min-width: 0; color: var(--p-doux); font-size: 13px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.draco-composer-enreg .enreg-onde { display: flex; align-items: center; gap: 3px; height: 26px; flex: none; }
.draco-composer-enreg .enreg-onde span { width: 4px; height: 30%; border-radius: 2px; background: #2f6fed; }
.draco-composer.en-cours .enreg-onde span { animation: onde 1.05s ease-in-out infinite; }
.draco-composer-enreg .enreg-onde span:nth-child(3n+2) { animation-delay: .14s; }
.draco-composer-enreg .enreg-onde span:nth-child(3n) { animation-delay: .28s; }
@keyframes onde { 0%, 100% { height: 22%; } 50% { height: 96%; } }
/* Pièces jointes dans une bulle utilisateur */
.draco-msg-pieces { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.draco-msg-pieces img { max-width: 150px; max-height: 120px; border-radius: 10px; display: block; }
.draco-msg-pieces .piece-fichier {
display: inline-flex; align-items: center; gap: 7px;
background: var(--p-chip); border: 1px solid var(--p-chip-bord); color: var(--p-texte);
border-radius: 9px; padding: 6px 9px; font-size: 12px;
}
.draco-panneau-form button:disabled { opacity: .5; cursor: default; }
.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; }
+332 -23
View File
@@ -40,7 +40,10 @@
var convKind = null; // 'principale' | 'parallele'
var convTitre = "";
var etatConnu = false;
var domaineCharge = null; // domaine des discussions affichées
var consoleUrlCharge = null; // URL (disc+domaine+app) de l'iframe console
var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null;
var microEl = null, piecesEl = null, fichierEl = null;
var titreEl = null, tiroirEl = null;
var versionEl = null, versionConnue = "", versionMaj = 0;
@@ -68,11 +71,16 @@
return correspondance[h] || null;
}
function urlConsole() {
/* Le domaine de l'hôte est passé explicitement : la console
* vit sur une autre origine, son localStorage ne voit jamais
* celui de l'app hôte — sans ce paramètre, elle rouvrait
* toujours le domaine de sa propre origine, pas celui du site. */
var params = [];
var id = convId || convStockee();
if (id) params.push("disc=" + encodeURIComponent(id));
var app = appHote();
if (app) params.push("app=" + encodeURIComponent(app));
params.push("domain=" + encodeURIComponent(domaine()));
return API + "/" + (params.length ? "?" + params.join("&") : "");
}
@@ -147,7 +155,15 @@
function ouvrirConsole() {
assurerSurimpression();
if (!surCadre.getAttribute("src")) surCadre.src = urlConsole();
/* L'iframe n'est rechargée que si sa cible a changé
* (discussion, domaine ou app) : même cible = état conservé,
* cible différente = la console s'ajuste, au lieu de garder
* la discussion du premier chargement. */
var url = urlConsole();
if (!surCadre.getAttribute("src") || url !== consoleUrlCharge) {
surCadre.src = url;
consoleUrlCharge = url;
}
if (surOuverte) return;
surOuverte = true;
/* Naissance à la géométrie du panneau (ancrée à droite), puis
@@ -175,7 +191,12 @@
setTimeout(terminer, 380); // filet si transitionend ne vient pas
surEl.style.width = largeurPanneau() + "px";
}
function modeSidebarConsole() { retrecirConsole(); }
function modeSidebarConsole() {
retrecirConsole();
/* Retour au panneau : si le domaine a changé pendant que la
* console occupait tout l'écran, le panneau s'ajuste aussi. */
resynchroniserDomaine();
}
function fermerAgentConsole() { fermer(); retrecirConsole(); }
/* ── Thème : suivre l'app hôte ─────────────────────────────── */
@@ -246,13 +267,10 @@
'<div class="draco-poignee" title="Glisser pour régler la largeur — double-clic : largeur par défaut"></div>' +
'<div class="draco-panneau-hd">' +
' <span style="font-size:19px;color:#7c5cd6">✦</span>' +
' <div><div class="titre">Spark — agent commun</div>' +
' <div class="soustitre">Service d\'agents dracodev</div></div>' +
' <div class="draco-panneau-titres"><div class="titre">Spark — agent commun</div>' +
' <div class="soustitre"><span class="point"></span><span class="txt">Connexion au service…</span></div></div>' +
' <button type="button" class="fermer" aria-label="Fermer le panneau">✕</button>' +
'</div>' +
'<div class="draco-panneau-hd draco-panneau-etat" style="padding-top:8px;padding-bottom:8px">' +
' <span class="point"></span><span class="txt">Connexion au service…</span>' +
'</div>' +
'<div class="draco-disc-bar">' +
' <span class="d-titre">Discussion principale</span>' +
' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' +
@@ -266,17 +284,40 @@
' <button type="button" data-outil="missions">🚀 Missions</button>' +
'</div>' +
'<div class="draco-panneau-msgs"></div>' +
'<div class="draco-panneau-ft"><form class="draco-panneau-form">' +
' <input type="text" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark">' +
' <button type="submit">Envoyer</button></form>' +
' <div class="draco-panneau-note">Conversation réelle, sous-agents (J3) et missions longues (J5) vers les exécuteurs configurés ; toute écriture dans les apps passe par une approbation.</div>' +
'<div class="draco-panneau-ft">' +
' <div class="draco-puces">' +
' <button type="button" data-sugg="resume">Résumer ma journée</button>' +
' <button type="button" data-sugg="projets">Où en sont mes projets ?</button>' +
' <button type="button" data-sugg="chercher">Chercher partout…</button>' +
' </div>' +
' <form class="draco-panneau-form draco-composer">' +
' <div class="draco-composer-pieces" hidden></div>' +
' <div class="draco-composer-ligne">' +
' <button type="button" class="composer-btn" data-comp="plus" title="Joindre une image ou un document" aria-label="Joindre une image ou un document"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg></button>' +
' <textarea rows="1" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark"></textarea>' +
' <input type="file" accept="image/*,.pdf,.docx,.odt,.txt,.md,.html,.csv" multiple hidden>' +
' <button type="button" class="composer-btn" data-comp="micro" title="Dictée vocale" aria-label="Dictée vocale" hidden><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><path d="M12 19v3"/></svg></button>' +
' <button type="submit" class="composer-envoi" title="Envoyer" aria-label="Envoyer" hidden><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20V4.5"/><path d="m5.5 11 6.5-6.5 6.5 6.5"/></svg></button>' +
' </div>' +
' <div class="draco-composer-enreg" hidden>' +
' <button type="button" class="composer-btn danger" data-comp="enreg-annuler" title="Annuler la dictée" aria-label="Annuler la dictée"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>' +
' <span class="enreg-temps">0:00</span>' +
' <div class="enreg-onde" aria-hidden="true"></div>' +
' <span class="enreg-texte"></span>' +
' <button type="button" class="composer-btn" data-comp="enreg-stop" title="Arrêter et corriger la transcription" aria-label="Arrêter la dictée"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/></svg></button>' +
' <button type="button" class="composer-envoi" data-comp="enreg-envoyer" title="Arrêter et envoyer" aria-label="Arrêter et envoyer"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20V4.5"/><path d="m5.5 11 6.5-6.5 6.5 6.5"/></svg></button>' +
' </div>' +
' </form>' +
' <div class="draco-panneau-version" style="margin-top:6px;font-size:11.5px;opacity:.6" hidden></div>' +
'</div>';
document.body.appendChild(root);
msgsEl = root.querySelector(".draco-panneau-msgs");
etatEl = root.querySelector(".draco-panneau-etat");
inputEl = root.querySelector("input");
sendBtn = root.querySelector("button[type=submit]");
etatEl = root.querySelector(".draco-panneau-hd .soustitre");
inputEl = root.querySelector(".draco-composer textarea");
sendBtn = root.querySelector(".draco-composer button[type=submit]");
microEl = root.querySelector('[data-comp="micro"]');
piecesEl = root.querySelector(".draco-composer-pieces");
fichierEl = root.querySelector(".draco-composer input[type=file]");
titreEl = root.querySelector(".d-titre");
tiroirEl = root.querySelector(".draco-discussions");
versionEl = root.querySelector(".draco-panneau-version");
@@ -288,6 +329,42 @@
root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations);
root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions);
root.querySelector("form").addEventListener("submit", envoyer);
inputEl.addEventListener("input", majComposer);
inputEl.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" && !ev.shiftKey) { ev.preventDefault(); envoyerComposer(); }
});
root.querySelector('[data-comp="plus"]').addEventListener("click", function () { fichierEl.click(); });
fichierEl.addEventListener("change", function (ev) {
ajouterFichiers(ev.target.files);
ev.target.value = "";
});
microEl.addEventListener("click", demarrerDictee);
root.querySelector('[data-comp="enreg-stop"]').addEventListener("click", function () { arreterDictee("stop"); });
root.querySelector('[data-comp="enreg-annuler"]').addEventListener("click", function () { arreterDictee("annuler"); });
root.querySelector('[data-comp="enreg-envoyer"]').addEventListener("click", function () { arreterDictee("envoyer"); });
Array.prototype.forEach.call(root.querySelectorAll("[data-sugg]"), function (b) {
b.addEventListener("click", function () {
var mode = b.getAttribute("data-sugg");
if (mode === "resume") {
envoyerMessage("Résume ma journée : mes tâches du jour, les cartes en Révision et l'activité récente.", []);
} else if (mode === "projets") {
envoyerMessage("Où en sont mes projets ? Fais le point sur l'état des projets et du board.", []);
} else {
inputEl.value = "Cherche « » partout : ";
majComposer();
inputEl.focus();
inputEl.setSelectionRange(9, 9);
}
});
});
/* Domaine changé dans un autre onglet pendant que le panneau
* existe : l'événement storage ne part que vers les autres
* documents, donc ici il arrive bien. Le changement dans le
* même onglet est rattrapé à chaque ouverture (ouvrir()). */
window.addEventListener("storage", function (ev) {
if (ev.key === "draco_domain") resynchroniserDomaine();
});
majComposer();
brancherPoignee(root.querySelector(".draco-poignee"));
appliquerTheme();
surveillerTheme();
@@ -326,8 +403,13 @@
}
function majEtat(cls, texte) {
etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls;
etatEl.querySelector(".txt").textContent = texte;
/* Statut intégré au titre (sous-titre de l'en-tête) : plus de
* barre d'état séparée. Le point vert/rouge/gris reste lisible
* sans prendre une rangée entière. */
if (!etatEl) return;
etatEl.className = "soustitre " + cls;
var t = etatEl.querySelector(".txt");
if (t) t.textContent = texte;
}
function majTitre() {
titreEl.textContent = convTitre || "Discussion principale";
@@ -400,9 +482,21 @@
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); });
}
/* Recharge la discussion du domaine courant quand il a changé
* depuis le dernier chargement (ouverture du panneau, autre
* onglet, retour du mode console). Sans changement : rien —
* on ne casse jamais une conversation en cours pour rien. */
function resynchroniserDomaine() {
if (!root || domaine() === domaineCharge) return;
msgsEl.innerHTML = "";
tiroirEl.hidden = true;
demarrerDiscussions();
}
function demarrerDiscussions() {
chargerListeDiscussions()
.then(function (d) {
domaineCharge = domaine();
var precedente = convStockee();
var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []);
var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0];
@@ -413,6 +507,7 @@
chargerHistorique();
})
.catch(function () {
domaineCharge = null; // échec : la prochaine ouverture réessaiera
ajouterMsg("sys", "Discussions : service injoignable pour le moment.");
});
}
@@ -780,19 +875,232 @@
.catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); });
}
function envoyer(ev) {
ev.preventDefault();
var texte = inputEl.value.trim();
if (!texte) return;
/* ── Composer façon console ────────────────────────────────
* Mêmes règles que la console : + (images/documents, 4 max),
* textarea auto-extensible, micro affiché seulement sans texte
* ni pièce et si le navigateur le supporte, envoi rond bleu
* affiché seulement avec texte ou pièce. */
var piecesEnAttente = [];
var TYPES_IMAGES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
var TYPES_DOCUMENTS = ["application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.oasis.opendocument.text",
"text/plain", "text/markdown", "text/html", "text/csv"];
var TAILLE_MAX_IMAGE = 5 * 1024 * 1024;
var TAILLE_MAX_DOC = 10 * 1024 * 1024;
function majComposer() {
if (!inputEl) return;
var aTexte = !!(inputEl.value && inputEl.value.trim());
sendBtn.hidden = !(aTexte || piecesEnAttente.length);
if (microEl) microEl.hidden = !dicteeSupportee || aTexte || piecesEnAttente.length;
inputEl.style.height = "auto";
var sh = inputEl.scrollHeight;
inputEl.style.height = sh ? Math.min(sh, 152) + "px" : "";
}
function rendrePiecesEnAttente() {
piecesEl.innerHTML = "";
piecesEl.hidden = !piecesEnAttente.length;
piecesEnAttente.forEach(function (p, i) {
var carte = document.createElement("div");
carte.className = "piece-carte";
var apercu;
if (TYPES_IMAGES.indexOf(p.type) >= 0) {
apercu = document.createElement("img");
apercu.src = p.dataUrl; apercu.alt = p.nom;
} else {
apercu = document.createElement("span");
apercu.className = "piece-doc-ext";
var ext = document.createElement("span");
ext.className = "piece-ext";
ext.textContent = (p.nom || "").split(".").pop() || "doc";
apercu.appendChild(ext);
}
var nom = document.createElement("span");
nom.className = "piece-nom"; nom.textContent = p.nom; nom.title = p.nom;
var x = document.createElement("button");
x.type = "button"; x.className = "piece-retirer"; x.textContent = "✕";
x.title = "Retirer " + p.nom;
x.setAttribute("aria-label", "Retirer " + p.nom);
x.addEventListener("click", function () {
piecesEnAttente.splice(i, 1);
rendrePiecesEnAttente(); majComposer();
});
carte.appendChild(apercu); carte.appendChild(nom); carte.appendChild(x);
piecesEl.appendChild(carte);
});
}
function ajouterFichiers(fichiers) {
Array.prototype.forEach.call(fichiers, function (f) {
if (piecesEnAttente.length >= 4) {
ajouterMsg("sys", "Au plus 4 pièces par message.");
return;
}
var estImage = TYPES_IMAGES.indexOf(f.type) >= 0;
var estDoc = TYPES_DOCUMENTS.indexOf(f.type) >= 0;
if (!estImage && !estDoc) {
ajouterMsg("sys", "« " + f.name + " » refusée : images PNG, JPEG, GIF ou WebP, ou documents PDF, DOCX, ODT, TXT, MD, HTML ou CSV seulement.");
return;
}
if (estImage && f.size > TAILLE_MAX_IMAGE) {
ajouterMsg("sys", "« " + f.name + " » refusée : 5 Mo maximum par image.");
return;
}
if (estDoc && f.size > TAILLE_MAX_DOC) {
ajouterMsg("sys", "« " + f.name + " » refusée : 10 Mo maximum par document.");
return;
}
var lecteur = new FileReader();
lecteur.onload = function () {
var dataUrl = String(lecteur.result || "");
piecesEnAttente.push({
nom: f.name, type: f.type,
base64: dataUrl.split(",")[1] || "", dataUrl: dataUrl
});
rendrePiecesEnAttente(); majComposer();
};
lecteur.readAsDataURL(f);
});
}
function envoyerComposer() {
var t = inputEl.value;
var pieces = piecesEnAttente;
piecesEnAttente = [];
inputEl.value = "";
ajouterMsg("user", texte);
rendrePiecesEnAttente();
majComposer();
envoyerMessage(t, pieces);
}
/* ── Dictée vocale — Web Speech API, 100 % navigateur, comme
* la console (fr-CA, résultats intérimaires en direct). */
var RecoVoc = window.SpeechRecognition || window.webkitSpeechRecognition;
var dicteeSupportee = !!RecoVoc;
var reco = null, enregEnCours = false, enregDebut = 0, enregMinuteur = null;
var texteAvantDictee = "", transcriptionFinale = "";
var modeFinDictee = "stop", dicteeErreur = null;
function demarrerDictee() {
if (!dicteeSupportee || enregEnCours) return;
texteAvantDictee = inputEl.value;
transcriptionFinale = "";
modeFinDictee = "stop";
dicteeErreur = null;
try { reco = new RecoVoc(); } catch (e) { reco = null; return; }
reco.lang = "fr-CA";
reco.interimResults = true;
reco.continuous = true;
reco.onresult = function (ev) {
var interim = "";
for (var i = ev.resultIndex; i < ev.results.length; i++) {
var r = ev.results[i];
if (r.isFinal) transcriptionFinale += r[0].transcript;
else interim += r[0].transcript;
}
var affiche = (texteAvantDictee + " " + transcriptionFinale + interim).trim();
root.querySelector(".enreg-texte").textContent = affiche;
inputEl.value = affiche;
};
reco.onerror = function (ev) {
if (ev.error === "aborted") return;
dicteeErreur = ev.error || "erreur";
modeFinDictee = "annuler";
};
reco.onend = terminerDictee;
try { reco.start(); } catch (e) { reco = null; return; }
enregEnCours = true;
enregDebut = Date.now();
root.querySelector(".enreg-temps").textContent = "0:00";
root.querySelector(".enreg-texte").textContent = "";
var onde = root.querySelector(".enreg-onde");
if (!onde.children.length) {
for (var j = 0; j < 14; j++) onde.appendChild(document.createElement("span"));
}
root.querySelector(".draco-composer-ligne").hidden = true;
root.querySelector(".draco-composer-enreg").hidden = false;
root.querySelector(".draco-composer").classList.add("en-cours");
enregMinuteur = window.setInterval(function () {
var s = Math.floor((Date.now() - enregDebut) / 1000);
root.querySelector(".enreg-temps").textContent = Math.floor(s / 60) + ":" + ("0" + (s % 60)).slice(-2);
}, 250);
}
function arreterDictee(mode) {
modeFinDictee = mode;
if (reco) {
try { reco.stop(); } catch (e) { terminerDictee(); }
} else {
terminerDictee();
}
}
function terminerDictee() {
if (!enregEnCours) return;
enregEnCours = false;
if (enregMinuteur) { window.clearInterval(enregMinuteur); enregMinuteur = null; }
reco = null;
root.querySelector(".draco-composer-ligne").hidden = false;
root.querySelector(".draco-composer-enreg").hidden = true;
root.querySelector(".draco-composer").classList.remove("en-cours");
if (modeFinDictee === "annuler") {
inputEl.value = texteAvantDictee;
if (dicteeErreur) {
ajouterMsg("sys", "Dictée vocale interrompue (" + dicteeErreur + ") — micro refusé ou indisponible ?");
dicteeErreur = null;
}
} else {
inputEl.value = (texteAvantDictee + " " + transcriptionFinale).trim();
}
var mode = modeFinDictee;
modeFinDictee = "stop";
majComposer();
if (mode === "envoyer") envoyerComposer();
else inputEl.focus();
}
function envoyer(ev) {
if (ev) ev.preventDefault();
envoyerComposer();
}
function envoyerMessage(texte, pieces) {
texte = (texte || "").trim();
pieces = pieces || [];
if (!texte && !pieces.length) return;
var el = ajouterMsg("user", texte);
if (pieces.length) {
var zone = document.createElement("div");
zone.className = "draco-msg-pieces";
pieces.forEach(function (p) {
if (TYPES_IMAGES.indexOf(p.type) >= 0) {
var img = document.createElement("img");
img.src = p.dataUrl; img.alt = p.nom;
zone.appendChild(img);
} else {
var past = document.createElement("span");
past.className = "piece-fichier";
past.textContent = p.nom;
zone.appendChild(past);
}
});
el.appendChild(zone);
}
sendBtn.disabled = true;
var attente = ajouterMsg("agent", "Spark réfléchit…");
var corps = { message: texte, domain: domaine(), conversation_id: convId };
if (pieces.length) {
corps.pieces = pieces.map(function (p) {
return { nom: p.nom, type: p.type, donnees_base64: p.base64 };
});
}
fetch(API + "/api/v1/chat", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: texte, domain: domaine(), conversation_id: convId })
body: JSON.stringify(corps)
})
.then(function (r) {
if (r.status === 401) {
@@ -831,11 +1139,12 @@
attente.textContent = "Service injoignable. Vérifie que le conteneur dracodev-agent est démarré, puis réessaie.";
majEtat("err", "Service d'agents injoignable");
})
.finally(function () { sendBtn.disabled = false; inputEl.focus(); });
.finally(function () { sendBtn.disabled = false; majComposer(); inputEl.focus(); });
}
function ouvrir() {
if (!root) { construire(); demarrerDiscussions(); }
else resynchroniserDomaine();
root.classList.add("open");
appliquerTheme();
verifierIdentite();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.81.1",
"version": "2.81.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": {