From b3835e79775c5917d25da34b7491315067e6520f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 11:47:01 +0000 Subject: [PATCH] =?UTF-8?q?Barre=20lat=C3=A9rale=20de=20la=20maquette=20+?= =?UTF-8?q?=20contexte=20workspace=20conditionnel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remaniement de la navigation selon « Loki App.dc.html » : le rail d'icônes de 60 px et le panneau de sessions laissent place à UNE barre latérale de 264 px — marque, navigation libellée (Demander, Historique, Fichiers, Outils), section Configuration, état Ollama avec jauge VRAM et carte de compte. La marque quitte la barre supérieure, qui ne garde que le contexte de la conversation. Les sessions restent accessibles par le sélecteur de la barre supérieure, et les fichiers par la vue Fichiers. Performance : l'inventaire du workspace n'est plus injecté à CHAQUE message, seulement quand la demande touche au code ou qu'un travail de code est en cours. Un simple « bonjour » partait avec des milliers de jetons de contexte, d'où une longue phase de traitement avant le premier jeton. L'attente avant le premier jeton affiche désormais le modèle en cours de chargement et le temps écoulé, au lieu de rester figée sur « Connexion à Ollama… » sans distinguer lenteur et blocage. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N --- backend/app/routes/chat.py | 41 +++++++- frontend/src/App.tsx | 6 +- frontend/src/components/Sidebar.tsx | 146 ++++++++++++++++++++++++++++ frontend/src/components/TopBar.tsx | 29 +++--- 4 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 frontend/src/components/Sidebar.tsx diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py index 2d5e8a0..3e1a3cc 100644 --- a/backend/app/routes/chat.py +++ b/backend/app/routes/chat.py @@ -14,6 +14,7 @@ import json import logging import os import re +import time from contextlib import suppress from fastapi import APIRouter, HTTPException @@ -61,6 +62,19 @@ def _apply_mode(cfg: dict, mode: str) -> dict: return cfg +def _loading_message(model: str, seconds: float) -> str: + """Message d'attente avant le premier jeton (chargement du modèle).""" + waited = int(seconds) + delay = f"{waited // 60} min" if waited >= 60 else f"{waited} s" + text = f"Chargement de {model} en mémoire… ({delay})" + if waited >= 90: + # On CONSTATE l'attente sans présumer de la cause : selon la machine, + # un gros modèle peut se répartir sur plusieurs GPU, ou déborder sur + # le CPU. Le panneau Matériel donne le placement réel. + text += " — placement réel visible dans Réglages › Matériel." + return text + + def _merge_system(convo: list[dict], extras: list[str]) -> None: """Garantit UN SEUL message système, en première position. @@ -415,16 +429,22 @@ async def chat(req: ChatRequest) -> StreamingResponse: # échouer toute la requête en 400. extras: list[str] = [] + # La demande touche-t-elle au code / aux fichiers ? Calculé AVANT les + # injections : tout ce qui suit est conditionné à cette réponse. + is_code_like = use_code or msg_router.is_code_task(req.content) + needs_workspace = is_code_like or prev_code + if memories: extras.append( "Souvenirs pertinents d'anciennes sessions :\n" + "\n---\n".join(memories) ) - # État du workspace injecté chaque tour : sans ça le modèle ignore - # quels fichiers existent et régurgite du code en chat au lieu de - # modifier le bon fichier. - listing = _workspace_listing() + # État du workspace : indispensable pour travailler sur les fichiers, + # mais inutile pour une simple discussion. On l'injectait à CHAQUE + # message — un « bonjour » partait avec des milliers de jetons de + # contexte, donc une longue phase de traitement avant le 1er jeton. + listing = _workspace_listing() if needs_workspace else [] if listing: recap, session_files = _session_code_context(history) parts = ["Fichiers du workspace : " + ", ".join(listing)] @@ -458,7 +478,6 @@ async def chat(req: ChatRequest) -> StreamingResponse: # Ponytail : méthode « code minimal » injectée pour toute tâche de code # (les deux chemins). Contre la sur-ingénierie qui casse les rendus. - is_code_like = use_code or msg_router.is_code_task(req.content) if cfg.get("ponytail", True) and is_code_like: extras.append(skills.PONYTAIL_GUIDANCE) @@ -574,6 +593,8 @@ async def chat(req: ChatRequest) -> StreamingResponse: await queue.put(None) producer = asyncio.create_task(produce_events()) + started = time.monotonic() + got_event = False try: while True: try: @@ -582,8 +603,18 @@ async def chat(req: ChatRequest) -> StreamingResponse: # Empêche OpenResty/Nginx/Cloudflare de fermer le SSE pendant # le chargement parfois long d'un modèle Ollama. yield _sse("ping", {"status": "waiting"}) + # …et DIT ce qui se passe : sans ça, l'interface restait sur + # « Connexion à Ollama… » pendant tout le chargement d'un + # gros modèle, sans moyen de distinguer lenteur et blocage. + if not got_event: + yield _sse("status", { + "message": _loading_message( + model, time.monotonic() - started + ) + }) continue + got_event = True if ev is None: break diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eba6e00..9cdaf9f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; -import { ActivityBar, type View } from "./components/ActivityBar"; +import { Sidebar, type View } from "./components/Sidebar"; import { TopBar } from "./components/TopBar"; -import { LeftPanel } from "./panels/LeftPanel"; import { ChatPanel } from "./panels/ChatPanel"; import { PreviewPanel } from "./panels/PreviewPanel"; import { SettingsView } from "./panels/SettingsView"; @@ -57,11 +56,10 @@ export default function App() {
- + {view === "chat" && ( <> - diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..9a6e4ed --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,146 @@ +import { useStore } from "../store/useStore"; +import { + ChatIcon, + ClockIcon, + FilesIcon, + LokiMark, + NodesIcon, + SettingsIcon, +} from "./Icon"; + +export type View = "chat" | "history" | "files" | "tools" | "settings"; + +const NAV: { id: View; label: string; icon: React.ReactNode }[] = [ + { id: "chat", label: "Demander", icon: }, + { id: "history", label: "Historique", icon: }, + { id: "files", label: "Fichiers", icon: }, + { id: "tools", label: "Outils", icon: }, +]; + +/** Barre latérale (264 px) : marque, navigation, état Ollama, compte. */ +export function Sidebar({ + active, + onChange, +}: { + active: View; + onChange: (v: View) => void; +}) { + const { status, selectedModel, systemStats } = useStore(); + const gpu = systemStats?.gpu ?? null; + + return ( + + ); +} + +function NavItem({ + label, + icon, + active, + onClick, +}: { + id: View; + label: string; + icon: React.ReactNode; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx index f11fc5a..098a218 100644 --- a/frontend/src/components/TopBar.tsx +++ b/frontend/src/components/TopBar.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "react"; import { useStore } from "../store/useStore"; import { getVersion } from "../api/client"; -import { LokiMark } from "./Icon"; import { SessionMenu } from "./SessionMenu"; -/** Barre supérieure sombre : logo, fil d'Ariane, statut Ollama, sélecteur. */ +/** Barre supérieure : conversation courante, ressources, statut Ollama. */ export function TopBar() { const status = useStore((s) => s.status); const stats = useStore((s) => s.systemStats); @@ -17,23 +16,17 @@ export function TopBar() { return (
-
- -
LOKI
-
- agent local - {version && version !== "dev" && ( - · {version.slice(0, 7)} - )} -
-
- -
- + {/* La marque vit désormais dans la barre latérale : on ne garde ici que + le contexte de la conversation en cours. */} + {version && version !== "dev" && ( + + {version.slice(0, 7)} + + )}