271 lines
9.7 KiB
JavaScript
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);
|
|
});
|
|
})();
|