Files
hermes-hub/static/js/hub.js
T

271 lines
9.7 KiB
JavaScript

/**
* Hermes Hub — Client-side Workspace Switcher & Context Manager
* Garantit l'isolation stricte du state front-end entre univers
* Intègre le sélecteur toggle DSH (Session vs Explorateur de fichiers)
*/
(function () {
let activeUniverse = "tt";
let activeMode = "session"; // "session" ou "files" (pour l'univers nabil)
const iframe = document.getElementById("workspace-iframe");
const loader = document.getElementById("hub-loader");
const activeTitle = document.getElementById("active-universe-name");
const activeScope = document.getElementById("active-universe-scope");
const activeTagline = document.getElementById("active-universe-tagline");
const nabilModeToggle = document.getElementById("nabil-mode-toggle");
const tabSession = document.getElementById("tab-session");
const tabFiles = document.getElementById("tab-files");
// Accent mappings
const ACCENT_MAP = {
tt: "var(--accent-tt)",
nyora: "var(--accent-nyora)",
perso: "var(--accent-perso)",
nabil: "var(--accent-nabil)"
};
const ACCENT_GLOW_MAP = {
tt: "var(--accent-tt-glow)",
nyora: "var(--accent-nyora-glow)",
perso: "var(--accent-perso-glow)",
nabil: "var(--accent-nabil-glow)"
};
function applyThemeTokens(universeId) {
const accent = ACCENT_MAP[universeId] || "var(--accent-tt)";
const glow = ACCENT_GLOW_MAP[universeId] || "var(--accent-tt-glow)";
document.documentElement.style.setProperty("--hub-accent-current", accent);
document.documentElement.style.setProperty("--hub-accent-glow-current", glow);
}
function getTargetUrl(universeId, mode) {
if (universeId === "nabil" && mode === "files") {
return "/u/nabil/files/";
}
return `/u/${universeId}/`;
}
function setMode(mode) {
activeMode = mode;
if (tabSession && tabFiles) {
if (mode === "session") {
tabSession.classList.add("active");
tabSession.setAttribute("aria-selected", "true");
tabFiles.classList.remove("active");
tabFiles.setAttribute("aria-selected", "false");
} else {
tabFiles.classList.add("active");
tabFiles.setAttribute("aria-selected", "true");
tabSession.classList.remove("active");
tabSession.setAttribute("aria-selected", "false");
}
}
// Recharger le canvas avec le mode choisi
loader.classList.remove("hidden");
iframe.src = "about:blank";
setTimeout(() => {
iframe.src = getTargetUrl(activeUniverse, activeMode);
}, 50);
}
function switchUniverse(universeId) {
if (!universeId) return;
const btn = document.querySelector(`.hub-universe-btn[data-universe="${universeId}"]`);
if (!btn) return;
// 1. ISOLATION TOTALE DU STATE CLIENT
// Décharger immédiatement l'iframe précédente pour purger la mémoire JS
loader.classList.remove("hidden");
iframe.src = "about:blank";
// 2. Mettre à jour l'univers actif
activeUniverse = universeId;
activeMode = "session"; // Réinitialiser le mode à session par défaut
// 3. Mettre à jour la classe active sur la sidebar
document.querySelectorAll(".hub-universe-btn").forEach((el) => el.classList.remove("active"));
btn.classList.add("active");
// 4. Appliquer les Design Tokens dynamiques
applyThemeTokens(universeId);
// 5. Mettre à jour les informations du header
const name = btn.dataset.name || universeId.toUpperCase();
const scope = btn.dataset.scope || universeId;
const tagline = btn.dataset.tagline || "";
const supportsFiles = btn.dataset.supportsFiles === "true";
if (activeTitle) activeTitle.textContent = name;
if (activeScope) activeScope.textContent = `Scope: ${scope}`;
if (activeTagline) activeTagline.textContent = tagline;
// 6. Afficher ou masquer le sélecteur toggle DSH (Claude Design)
if (nabilModeToggle) {
if (supportsFiles) {
nabilModeToggle.classList.add("visible");
// Reset tabs visual state
if (tabSession && tabFiles) {
tabSession.classList.add("active");
tabSession.setAttribute("aria-selected", "true");
tabFiles.classList.remove("active");
tabFiles.setAttribute("aria-selected", "false");
}
} else {
nabilModeToggle.classList.remove("visible");
}
}
// 7. Charger le nouvel univers via le proxy dédié
setTimeout(() => {
iframe.src = getTargetUrl(activeUniverse, activeMode);
}, 50);
}
// Écouter le chargement de l'iframe
if (iframe) {
iframe.addEventListener("load", () => {
if (iframe.src !== "about:blank") {
loader.classList.add("hidden");
}
});
}
// Polling de l'état de santé des univers
async function refreshHealthStatus() {
try {
const res = await fetch("/api/universes");
if (!res.ok) return;
const data = await res.json();
data.forEach((u) => {
const dot = document.getElementById(`status-dot-${u.id}`);
if (dot) {
if (u.health && u.health.status === "online") {
dot.className = "hub-status-dot online";
dot.title = `En ligne (${u.health.latency_ms}ms)`;
} else {
dot.className = "hub-status-dot";
dot.title = `Hors ligne ou dégradé (${u.health.status})`;
}
}
});
} catch (e) {
console.warn("Health check error:", e);
}
}
// Modales Personnalité & Clonage
const personaModal = document.getElementById("persona-modal");
const cloneModal = document.getElementById("clone-modal");
async function openPersonaModal() {
try {
const res = await fetch(`/api/universes/${activeUniverse}/persona`);
if (!res.ok) throw new Error("Erreur de chargement de la persona");
const persona = await res.json();
document.getElementById("persona-modal-title").textContent = `Personnalité — ${activeUniverse.toUpperCase()}`;
document.getElementById("persona-name").value = persona.name || "";
document.getElementById("persona-tagline").value = persona.tagline || "";
document.getElementById("persona-tone").value = persona.tone || "";
document.getElementById("persona-style").value = persona.style || "";
document.getElementById("persona-principles").value = (persona.principles || []).join("\n");
document.getElementById("persona-prompt").value = persona.system_prompt || "";
personaModal.classList.add("open");
} catch (e) {
alert("Impossible de charger la personnalité : " + e.message);
}
}
async function savePersona() {
const principlesText = document.getElementById("persona-principles").value;
const payload = {
universe_id: activeUniverse,
name: document.getElementById("persona-name").value,
tagline: document.getElementById("persona-tagline").value,
tone: document.getElementById("persona-tone").value,
style: document.getElementById("persona-style").value,
principles: principlesText.split("\n").map(s => s.trim()).filter(Boolean),
system_prompt: document.getElementById("persona-prompt").value,
version: 1
};
try {
const res = await fetch(`/api/universes/${activeUniverse}/persona`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error("Erreur de sauvegarde");
personaModal.classList.remove("open");
} catch (e) {
alert("Erreur lors de la sauvegarde : " + e.message);
}
}
function openCloneModal() {
document.getElementById("clone-source-name").textContent = activeUniverse.toUpperCase();
document.getElementById("clone-name").value = `Clone ${activeUniverse.toUpperCase()} - Test`;
cloneModal.classList.add("open");
}
async function submitClone() {
const cloneName = document.getElementById("clone-name").value;
if (!cloneName) return alert("Veuillez saisir un nom pour le clone.");
try {
const res = await fetch(`/api/universes/${activeUniverse}/clone`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ clone_name: cloneName })
});
if (!res.ok) throw new Error("Erreur lors du clonage");
const data = await res.json();
alert(`Profil cloné avec succès : ${data.clone.clone_id}`);
cloneModal.classList.remove("open");
} catch (e) {
alert("Erreur de clonage : " + e.message);
}
}
// Setup Event Listeners
document.addEventListener("DOMContentLoaded", () => {
// Boutons de changement d'univers
document.querySelectorAll(".hub-universe-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const uId = btn.dataset.universe;
switchUniverse(uId);
});
});
// Toggle Mode DSH (Session vs Files)
tabSession?.addEventListener("click", () => setMode("session"));
tabFiles?.addEventListener("click", () => setMode("files"));
// Outils Footer
document.getElementById("btn-edit-persona")?.addEventListener("click", openPersonaModal);
document.getElementById("btn-clone-universe")?.addEventListener("click", openCloneModal);
document.getElementById("btn-save-persona")?.addEventListener("click", savePersona);
document.getElementById("btn-submit-clone")?.addEventListener("click", submitClone);
// Boutons fermer modales
document.querySelectorAll(".hub-modal-close").forEach((btn) => {
btn.addEventListener("click", () => {
personaModal.classList.remove("open");
cloneModal.classList.remove("open");
});
});
// Démarrage : activer le premier univers (TT)
switchUniverse("tt");
// Lancer la vérification de santé
refreshHealthStatus();
setInterval(refreshHealthStatus, 15000);
});
})();