feat: initialisation Hermes Hub — interface multi-univers securisee (FastAPI, proxy async, design tokens)
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Hermes Hub — Client-side Workspace Switcher & Context Manager
|
||||
* Garantit l'isolation stricte du state front-end entre univers
|
||||
*/
|
||||
|
||||
(function () {
|
||||
let activeUniverse = "tt";
|
||||
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");
|
||||
|
||||
// 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 switchUniverse(universeId) {
|
||||
if (!universeId || (activeUniverse === universeId && iframe.src !== "about:blank")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector(`.hub-universe-btn[data-universe="${universeId}"]`);
|
||||
if (!btn) return;
|
||||
|
||||
// 1. ISOLATION TOTALE DU STATE CLIENT
|
||||
// Forcer le déchargement de l'iframe précédente pour éliminer tout résidu JS en mémoire
|
||||
loader.classList.remove("hidden");
|
||||
iframe.src = "about:blank";
|
||||
|
||||
// 2. Mettre à jour l'univers actif
|
||||
activeUniverse = universeId;
|
||||
|
||||
// 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 || "";
|
||||
|
||||
if (activeTitle) activeTitle.textContent = name;
|
||||
if (activeScope) activeScope.textContent = `Scope: ${scope}`;
|
||||
if (activeTagline) activeTagline.textContent = tagline;
|
||||
|
||||
// 6. Charger le nouvel univers via le proxy dédié
|
||||
const targetProxyUrl = `/u/${universeId}/`;
|
||||
setTimeout(() => {
|
||||
iframe.src = targetProxyUrl;
|
||||
}, 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Personnalité
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user