feat(phase4): permanent cache-control & asset versioning, canonical nabil stored_session_id & resume, dsh session persistence fix, dsh dialogue/files toggle, and collapsible conv sidebar
This commit is contained in:
+27
-6
@@ -114,7 +114,7 @@ async def list_universe_conversations(universe_id: str) -> List[Dict[str, Any]]:
|
|||||||
for s in backend_sessions:
|
for s in backend_sessions:
|
||||||
sid = str(s.get("id") or s.get("session_id") or "")
|
sid = str(s.get("id") or s.get("session_id") or "")
|
||||||
if sid:
|
if sid:
|
||||||
stitle = s.get("title") or s.get("display_name") or f"Session {sid[:8]}"
|
stitle = s.get("title") or s.get("display_name") or f"Session {sid[:15]}"
|
||||||
spinned = 1 if s.get("pinned") else 0
|
spinned = 1 if s.get("pinned") else 0
|
||||||
started_at = s.get("started_at")
|
started_at = s.get("started_at")
|
||||||
c_at = int(started_at * 1000) if isinstance(started_at, (int, float)) and started_at < 1e11 else int(time.time() * 1000)
|
c_at = int(started_at * 1000) if isinstance(started_at, (int, float)) and started_at < 1e11 else int(time.time() * 1000)
|
||||||
@@ -171,9 +171,10 @@ async def create_universe_conversation(universe_id: str, title: Optional[str] =
|
|||||||
data = json.loads(raw)
|
data = json.loads(raw)
|
||||||
if data.get("id") == req_id and "result" in data:
|
if data.get("id") == req_id and "result" in data:
|
||||||
res = data["result"]
|
res = data["result"]
|
||||||
session_key = res.get("session_id") or res.get("stored_session_id")
|
# Prioritize durable stored_session_id (format YYYYMMDD_HHMMSS_xxxxxx) over ephemeral handle
|
||||||
|
session_key = res.get("stored_session_id") or res.get("session_id")
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# Fallback if WS fails
|
# Fallback if WS fails
|
||||||
session_key = f"nabil_{uuid.uuid4().hex[:12]}"
|
session_key = f"nabil_{uuid.uuid4().hex[:12]}"
|
||||||
|
|
||||||
@@ -234,7 +235,6 @@ async def get_conversation_messages(universe_id: str, session_key: str) -> List[
|
|||||||
|
|
||||||
async def stream_chat_messages(universe_id: str, session_key: str, message: str) -> AsyncGenerator[str, None]:
|
async def stream_chat_messages(universe_id: str, session_key: str, message: str) -> AsyncGenerator[str, None]:
|
||||||
"""Streams chat chunks using Server-Sent Events (SSE)."""
|
"""Streams chat chunks using Server-Sent Events (SSE)."""
|
||||||
# Touch conversation in SQLite and update title if first message
|
|
||||||
existing = get_conversation(universe_id, session_key)
|
existing = get_conversation(universe_id, session_key)
|
||||||
if existing and existing.get("title") in ("Nouvelle conversation", "Untitled", None):
|
if existing and existing.get("title") in ("Nouvelle conversation", "Untitled", None):
|
||||||
summary_title = (message[:38] + "...") if len(message) > 38 else message
|
summary_title = (message[:38] + "...") if len(message) > 38 else message
|
||||||
@@ -272,13 +272,35 @@ async def stream_chat_messages(universe_id: str, session_key: str, message: str)
|
|||||||
ping_interval=20,
|
ping_interval=20,
|
||||||
ping_timeout=20
|
ping_timeout=20
|
||||||
) as ws:
|
) as ws:
|
||||||
|
# 1. First resume/activate the session to get the active live handle
|
||||||
|
resume_req_id = f"res-{uuid.uuid4().hex[:8]}"
|
||||||
|
await ws.send(json.dumps({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": resume_req_id,
|
||||||
|
"method": "session.resume",
|
||||||
|
"params": {"session_id": session_key}
|
||||||
|
}))
|
||||||
|
|
||||||
|
active_sid = session_key
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
raw_res = await asyncio.wait_for(ws.recv(), timeout=4.0)
|
||||||
|
data_res = json.loads(raw_res)
|
||||||
|
if data_res.get("id") == resume_req_id:
|
||||||
|
if "result" in data_res:
|
||||||
|
active_sid = data_res["result"].get("session_id") or session_key
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. Submit prompt with active_sid
|
||||||
req_id = f"prompt-{uuid.uuid4().hex[:8]}"
|
req_id = f"prompt-{uuid.uuid4().hex[:8]}"
|
||||||
send_payload = {
|
send_payload = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": req_id,
|
"id": req_id,
|
||||||
"method": "prompt.submit",
|
"method": "prompt.submit",
|
||||||
"params": {
|
"params": {
|
||||||
"session_id": session_key,
|
"session_id": active_sid,
|
||||||
"text": message
|
"text": message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,7 +322,6 @@ async def stream_chat_messages(universe_id: str, session_key: str, message: str)
|
|||||||
yield f"event: chunk\ndata: {json.dumps({'text': accumulated_text, 'chunk': delta, 'fullReplace': True})}\n\n"
|
yield f"event: chunk\ndata: {json.dumps({'text': accumulated_text, 'chunk': delta, 'fullReplace': True})}\n\n"
|
||||||
elif ev_type in ("reasoning.delta", "thinking.delta"):
|
elif ev_type in ("reasoning.delta", "thinking.delta"):
|
||||||
delta = payload.get("text", "")
|
delta = payload.get("text", "")
|
||||||
# We can stream reasoning as needed
|
|
||||||
elif ev_type in ("message.finish", "session.idle", "reasoning.available") or data.get("method") in ("chat.done", "session.done"):
|
elif ev_type in ("message.finish", "session.idle", "reasoning.available") or data.get("method") in ("chat.done", "session.done"):
|
||||||
yield f"event: done\ndata: {json.dumps({'state': 'complete', 'text': accumulated_text})}\n\n"
|
yield f"event: done\ndata: {json.dumps({'state': 'complete', 'text': accumulated_text})}\n\n"
|
||||||
break
|
break
|
||||||
|
|||||||
+20
-5
@@ -1,10 +1,11 @@
|
|||||||
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
import time
|
||||||
|
|
||||||
from app.config import settings, UNIVERSES, BASE_DIR
|
from app.config import settings, UNIVERSES, BASE_DIR
|
||||||
from app.routers import api, proxy, chat
|
from app.routers import api, proxy, chat
|
||||||
@@ -12,6 +13,8 @@ from app.proxy import close_http_client, proxy_request, proxy_websocket
|
|||||||
from app.personas import ensure_dirs
|
from app.personas import ensure_dirs
|
||||||
from app.db import init_db
|
from app.db import init_db
|
||||||
|
|
||||||
|
APP_VERSION = f"2.1.{int(time.time())}"
|
||||||
|
|
||||||
def get_subdomain_target(host: str) -> Optional[Tuple[str, str]]:
|
def get_subdomain_target(host: str) -> Optional[Tuple[str, str]]:
|
||||||
"""
|
"""
|
||||||
Inspects Host header and returns (target_backend_url, universe_id) if it matches a universe hostname.
|
Inspects Host header and returns (target_backend_url, universe_id) if it matches a universe hostname.
|
||||||
@@ -22,7 +25,7 @@ def get_subdomain_target(host: str) -> Optional[Tuple[str, str]]:
|
|||||||
perso-hub.yesminedor.tn -> (HERMES_PERSO_URL, 'perso')
|
perso-hub.yesminedor.tn -> (HERMES_PERSO_URL, 'perso')
|
||||||
nabil-hub.yesminedor.tn -> (HERMES_NABIL_URL, 'nabil')
|
nabil-hub.yesminedor.tn -> (HERMES_NABIL_URL, 'nabil')
|
||||||
dsh-hub.yesminedor.tn -> (HERMES_DSH_URL, 'dsh')
|
dsh-hub.yesminedor.tn -> (HERMES_DSH_URL, 'dsh')
|
||||||
files-hub.yesminedor.tn -> (DSH_FILEBROWSER_URL, 'nabil')
|
files-hub.yesminedor.tn -> (DSH_FILEBROWSER_URL, 'dsh')
|
||||||
|
|
||||||
Apex / Hub UI:
|
Apex / Hub UI:
|
||||||
hub.yesminedor.tn -> None (Serves index.html Workspace Switcher)
|
hub.yesminedor.tn -> None (Serves index.html Workspace Switcher)
|
||||||
@@ -54,7 +57,7 @@ def get_subdomain_target(host: str) -> Optional[Tuple[str, str]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if sub in ("files", "dsh-files"):
|
if sub in ("files", "dsh-files"):
|
||||||
return (settings.dsh_filebrowser_url, "nabil")
|
return (settings.dsh_filebrowser_url, "dsh")
|
||||||
|
|
||||||
if sub in UNIVERSES:
|
if sub in UNIVERSES:
|
||||||
return (UNIVERSES[sub].backend_url, sub)
|
return (UNIVERSES[sub].backend_url, sub)
|
||||||
@@ -70,10 +73,21 @@ async def lifespan(app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.app_name,
|
title=settings.app_name,
|
||||||
version="1.0.0",
|
version="2.1.0",
|
||||||
lifespan=lifespan
|
lifespan=lifespan
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cache-Control & Anti-Stale Middleware for Hub UI & Statics
|
||||||
|
@app.middleware("http")
|
||||||
|
async def cache_control_middleware(request: Request, call_next):
|
||||||
|
response: Response = await call_next(request)
|
||||||
|
path = request.url.path
|
||||||
|
if path.startswith("/static/") or path == "/" or path.endswith(".html"):
|
||||||
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0"
|
||||||
|
response.headers["Pragma"] = "no-cache"
|
||||||
|
response.headers["Expires"] = "0"
|
||||||
|
return response
|
||||||
|
|
||||||
# Subdomain Routing Middleware for HTTP
|
# Subdomain Routing Middleware for HTTP
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def subdomain_routing_middleware(request: Request, call_next):
|
async def subdomain_routing_middleware(request: Request, call_next):
|
||||||
@@ -112,7 +126,8 @@ async def index_view(request: Request):
|
|||||||
name="index.html",
|
name="index.html",
|
||||||
context={
|
context={
|
||||||
"universes": UNIVERSES,
|
"universes": UNIVERSES,
|
||||||
"app_name": settings.app_name
|
"app_name": settings.app_name,
|
||||||
|
"version": APP_VERSION
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
<title>{% block title %}{{ app_name }}{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Design Tokens System (Substituable par Claude Design) -->
|
<!-- Design Tokens System & Anti-Stale Assets -->
|
||||||
<link rel="stylesheet" href="/static/css/tokens.css">
|
<link rel="stylesheet" href="/static/css/tokens.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css?v={{ version }}">
|
||||||
|
|
||||||
{% block extra_head %}{% endblock %}
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<script src="/static/js/hub.js"></script>
|
<script src="/static/js/hub.js?v={{ version }}"></script>
|
||||||
{% block extra_scripts %}{% endblock %}
|
{% block extra_scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="hub-brand-left">
|
<div class="hub-brand-left">
|
||||||
<div class="hub-logo-icon">H</div>
|
<div class="hub-logo-icon">H</div>
|
||||||
<div class="hub-brand-text">Hermes Hub</div>
|
<div class="hub-brand-text">Hermes Hub</div>
|
||||||
<div class="hub-brand-badge">v2.0</div>
|
<div class="hub-brand-badge">v2.1</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-toggle-sidebar" class="hub-collapse-btn" type="button" aria-label="Replier la barre latérale" title="Replier la barre latérale">
|
<button id="btn-toggle-sidebar" class="hub-collapse-btn" type="button" aria-label="Replier la barre latérale" title="Replier la barre latérale">
|
||||||
<svg class="hub-collapse-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="hub-collapse-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -70,16 +70,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hub-topbar-right">
|
<div class="hub-topbar-right">
|
||||||
<!-- Sélecteur Toggle Mode DSH (Univers Nabil) -->
|
<!-- Sélecteur Toggle Mode DSH (Exclusif à l'univers DSH) -->
|
||||||
<div id="nabil-mode-toggle" role="tablist" aria-label="Mode du canvas" class="hub-mode-toggle" style="display: none;">
|
<div id="dsh-mode-toggle" role="tablist" aria-label="Mode DSH" class="hub-mode-toggle" style="display: none;">
|
||||||
<button id="tab-session" role="tab" aria-selected="true" class="hub-mode-tab active" type="button">
|
<button id="tab-session" role="tab" aria-selected="true" class="hub-mode-tab active" type="button">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
||||||
<span>Chat Natif</span>
|
<span>💬 Dialogue</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="hub-mode-divider"></div>
|
<div class="hub-mode-divider"></div>
|
||||||
<button id="tab-files" role="tab" aria-selected="false" class="hub-mode-tab" type="button">
|
<button id="tab-files" role="tab" aria-selected="false" class="hub-mode-tab" type="button">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 20h14a2 2 0 0 0 2-2V9H12L10 6H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1z"></path></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 20h14a2 2 0 0 0 2-2V9H12L10 6H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1z"></path></svg>
|
||||||
<span>Fichiers DSH</span>
|
<span>📁 Explorateur</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -89,15 +89,20 @@
|
|||||||
|
|
||||||
<!-- Canvas Container -->
|
<!-- Canvas Container -->
|
||||||
<div class="hub-canvas">
|
<div class="hub-canvas">
|
||||||
<!-- 1. Native Chat Two-Column Interface -->
|
<!-- 1. Native Chat Two-Column Interface (TT, Nyora, Perso, Nabil) -->
|
||||||
<div id="hub-native-chat" class="hub-native-chat-layout">
|
<div id="hub-native-chat" class="hub-native-chat-layout">
|
||||||
<!-- Sub-sidebar Conversations -->
|
<!-- Sub-sidebar Conversations -->
|
||||||
<div class="hub-conv-sidebar">
|
<div id="hub-conv-sidebar" class="hub-conv-sidebar">
|
||||||
<div class="hub-conv-header">
|
<div class="hub-conv-header">
|
||||||
<button id="btn-new-chat" class="hub-btn-new-chat" type="button">
|
<button id="btn-new-chat" class="hub-btn-new-chat" type="button">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||||
<span>Nouvelle discussion</span>
|
<span>Nouvelle discussion</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-toggle-conv-sidebar" class="hub-collapse-conv-btn" type="button" aria-label="Masquer la liste des discussions" title="Masquer la liste des discussions">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="15 18 9 12 15 6"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hub-conv-search-wrap">
|
<div class="hub-conv-search-wrap">
|
||||||
@@ -115,6 +120,11 @@
|
|||||||
<!-- Active Conversation Header -->
|
<!-- Active Conversation Header -->
|
||||||
<div class="hub-chat-header">
|
<div class="hub-chat-header">
|
||||||
<div class="hub-chat-header-info">
|
<div class="hub-chat-header-info">
|
||||||
|
<button id="btn-expand-conv-sidebar" class="hub-expand-conv-btn" type="button" aria-label="Afficher les discussions" title="Afficher les discussions" style="display: none;">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="9 18 15 12 9 6"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<span id="current-chat-title" class="hub-chat-header-title">Nouvelle conversation</span>
|
<span id="current-chat-title" class="hub-chat-header-title">Nouvelle conversation</span>
|
||||||
<span id="current-chat-status" class="hub-chat-header-status">Prêt</span>
|
<span id="current-chat-status" class="hub-chat-header-status">Prêt</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +165,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. Embedded Iframe View (Used for DSH client and DSH Filebrowser) -->
|
<!-- 2. Embedded Iframe View (Used exclusively for DSH Dialogue and DSH Files) -->
|
||||||
<div id="hub-iframe-wrapper" class="hub-iframe-wrapper" style="display: none;">
|
<div id="hub-iframe-wrapper" class="hub-iframe-wrapper" style="display: none;">
|
||||||
<div id="hub-loader" class="hub-loader-overlay">
|
<div id="hub-loader" class="hub-loader-overlay">
|
||||||
<div class="hub-spinner"></div>
|
<div class="hub-spinner"></div>
|
||||||
|
|||||||
@@ -420,11 +420,56 @@ body, html {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-conv-sidebar.collapsed {
|
||||||
|
width: 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
border-right: none;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-conv-header {
|
.hub-conv-header {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-bottom: 1px solid var(--hub-border-subtle);
|
border-bottom: 1px solid var(--hub-border-subtle);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-conv-header .hub-btn-new-chat {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-collapse-conv-btn,
|
||||||
|
.hub-expand-conv-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--hub-border-subtle);
|
||||||
|
border-radius: var(--hub-radius-sm);
|
||||||
|
color: var(--hub-text-muted);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-collapse-conv-btn:hover,
|
||||||
|
.hub-expand-conv-btn:hover {
|
||||||
|
color: var(--hub-text-primary);
|
||||||
|
background: var(--hub-bg-card-hover);
|
||||||
|
border-color: var(--hub-border-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-chat-header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hub-btn-new-chat {
|
.hub-btn-new-chat {
|
||||||
|
|||||||
+36
-13
@@ -6,11 +6,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Elements
|
// Elements
|
||||||
const sidebar = document.getElementById('hub-sidebar');
|
const sidebar = document.getElementById('hub-sidebar');
|
||||||
const btnToggleSidebar = document.getElementById('btn-toggle-sidebar');
|
const btnToggleSidebar = document.getElementById('btn-toggle-sidebar');
|
||||||
|
const convSidebar = document.getElementById('hub-conv-sidebar');
|
||||||
|
const btnToggleConvSidebar = document.getElementById('btn-toggle-conv-sidebar');
|
||||||
|
const btnExpandConvSidebar = document.getElementById('btn-expand-conv-sidebar');
|
||||||
const universeBtns = document.querySelectorAll('.hub-universe-btn');
|
const universeBtns = document.querySelectorAll('.hub-universe-btn');
|
||||||
const activeTitle = document.getElementById('active-universe-name');
|
const activeTitle = document.getElementById('active-universe-name');
|
||||||
const activeScope = document.getElementById('active-universe-scope');
|
const activeScope = document.getElementById('active-universe-scope');
|
||||||
const activeTagline = document.getElementById('active-universe-tagline');
|
const activeTagline = document.getElementById('active-universe-tagline');
|
||||||
const nabilModeToggle = document.getElementById('nabil-mode-toggle');
|
const dshModeToggle = document.getElementById('dsh-mode-toggle');
|
||||||
const tabSession = document.getElementById('tab-session');
|
const tabSession = document.getElementById('tab-session');
|
||||||
const tabFiles = document.getElementById('tab-files');
|
const tabFiles = document.getElementById('tab-files');
|
||||||
|
|
||||||
@@ -67,6 +70,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Conversation Sub-Sidebar Collapse Persistence
|
||||||
|
const isConvCollapsed = localStorage.getItem('hermes_hub_conv_collapsed') === 'true';
|
||||||
|
if (isConvCollapsed && convSidebar) {
|
||||||
|
convSidebar.classList.add('collapsed');
|
||||||
|
if (btnExpandConvSidebar) btnExpandConvSidebar.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnToggleConvSidebar && convSidebar) {
|
||||||
|
btnToggleConvSidebar.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
convSidebar.classList.add('collapsed');
|
||||||
|
if (btnExpandConvSidebar) btnExpandConvSidebar.style.display = 'inline-flex';
|
||||||
|
localStorage.setItem('hermes_hub_conv_collapsed', 'true');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnExpandConvSidebar && convSidebar) {
|
||||||
|
btnExpandConvSidebar.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
convSidebar.classList.remove('collapsed');
|
||||||
|
btnExpandConvSidebar.style.display = 'none';
|
||||||
|
localStorage.setItem('hermes_hub_conv_collapsed', 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Accent Mapping
|
// Accent Mapping
|
||||||
const ACCENT_MAP = {
|
const ACCENT_MAP = {
|
||||||
'tt': { accent: 'var(--accent-tt)', glow: 'var(--accent-tt-glow)' },
|
'tt': { accent: 'var(--accent-tt)', glow: 'var(--accent-tt-glow)' },
|
||||||
@@ -134,19 +162,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
updateThemeAccent(universeId);
|
updateThemeAccent(universeId);
|
||||||
|
|
||||||
// Show/hide Nabil Files Toggle
|
// Show/hide DSH Mode Toggle (Exclusif à l'univers DSH)
|
||||||
if (universeId === 'nabil') {
|
|
||||||
nabilModeToggle.style.display = 'inline-flex';
|
|
||||||
tabSession.classList.add('active');
|
|
||||||
tabFiles.classList.remove('active');
|
|
||||||
} else {
|
|
||||||
nabilModeToggle.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine layout: DSH uses embedded client, others use Native Chat
|
|
||||||
if (universeId === 'dsh') {
|
if (universeId === 'dsh') {
|
||||||
|
if (dshModeToggle) dshModeToggle.style.display = 'inline-flex';
|
||||||
|
if (tabSession) tabSession.classList.add('active');
|
||||||
|
if (tabFiles) tabFiles.classList.remove('active');
|
||||||
showIframeView(`https://dsh-hub.yesminedor.tn/`);
|
showIframeView(`https://dsh-hub.yesminedor.tn/`);
|
||||||
} else {
|
} else {
|
||||||
|
if (dshModeToggle) dshModeToggle.style.display = 'none';
|
||||||
showNativeChatView();
|
showNativeChatView();
|
||||||
loadConversations();
|
loadConversations();
|
||||||
}
|
}
|
||||||
@@ -617,12 +640,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nabil Mode Switcher (Chat Natif vs Fichiers DSH)
|
// DSH Mode Switcher (Dialogue DSH vs Explorateur de Fichiers)
|
||||||
if (tabSession) {
|
if (tabSession) {
|
||||||
tabSession.addEventListener('click', () => {
|
tabSession.addEventListener('click', () => {
|
||||||
tabSession.classList.add('active');
|
tabSession.classList.add('active');
|
||||||
tabFiles.classList.remove('active');
|
tabFiles.classList.remove('active');
|
||||||
showNativeChatView();
|
showIframeView(`https://dsh-hub.yesminedor.tn/`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user