Barre latérale de la maquette + contexte workspace conditionnel

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
Claude committed 2026-08-03 11:47:01 +00:00
1 parent d99ff11262
commit b3835e7977
4 files changed
+195 -27

No files matched your search

+2 -4
View File
@@ -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() {
<div className="flex h-full flex-col bg-base text-ink">
<TopBar />
<div className="flex min-h-0 flex-1">
<ActivityBar active={view} onChange={setView} />
<Sidebar active={view} onChange={setView} />
{view === "chat" && (
<>
<LeftPanel />
<ChatPanel />
<PreviewPanel />
</>
+146
View File
@@ -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: <ChatIcon /> },
{ id: "history", label: "Historique", icon: <ClockIcon /> },
{ id: "files", label: "Fichiers", icon: <FilesIcon /> },
{ id: "tools", label: "Outils", icon: <NodesIcon /> },
];
/** 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 (
<aside className="flex w-[264px] flex-none flex-col border-r border-line bg-panel px-3.5 py-4">
{/* Marque */}
<div className="flex items-center gap-2.5 px-1.5 pb-4">
<LokiMark size={26} />
<span className="text-[16px] font-medium tracking-tight text-ink">Loki</span>
<span className="ml-auto rounded-full border border-line px-2 py-0.5 text-[11px] text-muted-3">
local
</span>
</div>
<div className="mx-1.5 mb-3.5 h-px bg-line" />
<nav className="flex flex-col gap-[3px]">
{NAV.map((it) => (
<NavItem
key={it.id}
{...it}
active={active === it.id}
onClick={() => onChange(it.id)}
/>
))}
</nav>
<div className="font-pixel px-2.5 pb-2 pt-5 text-[10px] text-label">
Configuration
</div>
<NavItem
id="settings"
label="Modèle & génération"
icon={<SettingsIcon />}
active={active === "settings"}
onClick={() => onChange("settings")}
/>
<div className="flex-1" />
{/* État Ollama + GPU */}
<div className="rounded-card border border-line p-3">
<div className="mb-2 flex items-center gap-2">
<span
className={`h-1.5 w-1.5 rounded-full ${
status?.connected ? "bg-accent" : "bg-warn"
}`}
/>
<span className="text-[12.5px] text-ink-3">
{status?.connected ? "Ollama connecté" : "Ollama déconnecté"}
</span>
</div>
<div className="mb-2.5 truncate font-mono text-[12px] text-muted-3">
{selectedModel || "aucun modèle"}
</div>
{gpu && (
<>
<div className="h-1 overflow-hidden rounded-sm bg-line-soft">
<span
className="block h-full bg-accent"
style={{
width: `${Math.min(
100,
(gpu.vram_used_mb / Math.max(gpu.vram_total_mb, 1)) * 100
)}%`,
}}
/>
</div>
<div className="mt-2 text-[11.5px] text-muted-3">
VRAM {(gpu.vram_used_mb / 1000).toFixed(1)} /{" "}
{(gpu.vram_total_mb / 1000).toFixed(1)} Go · GPU{" "}
{Math.round(gpu.util_pct)} %
</div>
</>
)}
</div>
{/* Compte local */}
<div className="mt-3 flex items-center gap-2.5 rounded-card border border-line px-3 py-2.5">
<span className="grid h-[30px] w-[30px] flex-none place-items-center rounded-full bg-line-soft text-[12px] text-ink-3">
M
</span>
<span className="min-w-0">
<span className="block text-[13.5px] text-ink">Compte local</span>
<span className="block truncate font-mono text-[11.5px] text-muted-3">
{status?.host || "workspace"}
</span>
</span>
</div>
</aside>
);
}
function NavItem({
label,
icon,
active,
onClick,
}: {
id: View;
label: string;
icon: React.ReactNode;
active: boolean;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className={`flex w-full items-center gap-2.5 rounded-card border px-2.5 py-2 text-left text-[14px] ${
active
? "border-accent-line bg-accent-ghost text-ink"
: "border-transparent text-ink-3 hover:bg-accent-ghost hover:text-ink"
}`}
>
{icon}
{label}
</button>
);
}
+11 -18
View File
@@ -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 (
<div className="flex h-[54px] flex-none items-center gap-3.5 border-b border-line bg-bar px-4">
<div className="flex items-center gap-2.5">
<LokiMark />
<div className="font-pixel text-[14px] text-ink">LOKI</div>
<div
className="text-[13px] leading-none text-on-dark-2"
title={version ? `build ${version}` : undefined}
>
agent local
{version && version !== "dev" && (
<span className="ml-1.5 text-on-dark-3">· {version.slice(0, 7)}</span>
)}
</div>
</div>
<div className="h-6 w-[3px] bg-chrome-3" />
{/* La marque vit désormais dans la barre latérale : on ne garde ici que
le contexte de la conversation en cours. */}
<SessionMenu />
{version && version !== "dev" && (
<span
className="text-[12px] text-muted-3"
title={`build ${version}`}
>
{version.slice(0, 7)}
</span>
)}
<div className="flex-1" />