Files
Loki/frontend/src/panels/LeftPanel.tsx
T
MichaelandClaude Opus 4.8 cb2872c78a perf: fix model reload thrash and cut time-to-first-token
Ollama reloaded the chat model mid-message because plan/summary/router
calls sent divergent runner options (no num_ctx/num_batch) and omitted
keep_alive — the main cause of perceived slowness.

- share one keep-alive httpx.AsyncClient for all Ollama calls
- unify runner options (runner_options) + keep_alive on every model
  call, embeddings included
- drop the blocking LLM routing fallback (pure lexical heuristic)
- run RAG recall + plan + code-model pick in parallel inside the SSE
  stream, after the start event
- RAG cosine scoring off the event loop; cache /api/tags 30s and
  nvidia-smi 5s; frontend polls 2s->5s, warm poll backoff, dedup
  config fetch

feat: working session menu in TopBar (switch/create/rename/delete)
feat: workspace file deletion (DELETE /api/files + UI trash buttons)
docs: recommended Ollama env vars (KEEP_ALIVE, MAX_LOADED_MODELS...)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:58:18 +02:00

186 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect } from "react";
import { useStore } from "../store/useStore";
import { DownloadIcon, PlusIcon, TrashIcon } from "../components/Icon";
import { downloadFile, type FileNode } from "../api/client";
import { relTime } from "../lib/time";
/** Panneau gauche : historique des sessions + arborescence de fichiers. */
export function LeftPanel() {
const {
sessions,
currentSessionId,
streamingSessionId,
refreshSessions,
refreshFiles,
fileTree,
newSession,
openSession,
removeSession,
} = useStore();
useEffect(() => {
refreshSessions();
refreshFiles();
}, [refreshSessions, refreshFiles]);
return (
<div className="flex w-[280px] flex-none flex-col border-r-[3px] border-line bg-panel">
{/* Historique */}
<div className="flex items-center justify-between px-4 pb-2.5 pt-4">
<span className="font-pixel text-[10px] text-label">HISTORIQUE</span>
<button
onClick={newSession}
className="flex cursor-pointer items-center gap-1 text-[13px] text-accent"
>
<PlusIcon />
Nouvelle
</button>
</div>
<div className="scr flex max-h-[45%] flex-col gap-2 overflow-auto px-3 py-1">
{sessions.length === 0 && (
<div className="px-1 py-2 text-xs text-muted-2">
Aucune session. Lance une conversation ci-dessous.
</div>
)}
{sessions.map((s) => {
const active = s.id === currentSessionId;
const working = s.id === streamingSessionId;
return (
<div
key={s.id}
onClick={() => openSession(s.id)}
className={`group cursor-pointer border-[3px] border-line px-3 py-2.5 ${
active ? "bg-card-deep shadow-hard-accent" : "bg-card"
}`}
style={{ borderRadius: 7 }}
>
<div className="flex items-center gap-1">
<div
className={`flex-1 truncate text-[14px] leading-tight ${
active ? "text-white" : "text-ink-2"
}`}
>
{s.title}
</div>
{working && (
<span
className="flex-none border-2 border-line bg-accent px-1.5 py-0.5 text-[10px] leading-none text-white"
title="Session en cours de travail"
>
EN COURS
</span>
)}
<button
onClick={(e) => {
e.stopPropagation();
removeSession(s.id);
}}
className={`hidden group-hover:block ${
active ? "text-on-dark-2 hover:text-accent" : "text-muted-3 hover:text-warn"
}`}
title="Supprimer"
>
×
</button>
</div>
<div className="mt-1 text-[13px] text-muted-3">
{relTime(s.updated_at)} · {s.message_count ?? 0} message
{(s.message_count ?? 0) > 1 ? "s" : ""}
</div>
</div>
);
})}
</div>
<div className="mx-0 my-4 h-[3px] bg-line" />
{/* Fichiers */}
<div className="flex items-center justify-between px-4 pb-2">
<span className="font-pixel text-[10px] text-label">FICHIERS</span>
<span className="text-[13px] text-muted-3">workspace/</span>
</div>
<div className="scr flex-1 overflow-auto px-3 pb-3.5 text-[14px]">
{fileTree.length === 0 ? (
<div className="px-2 py-4 text-center text-muted-2">
Aucun fichier pour l'instant.
<br />
L'agent créera des fichiers ici.
</div>
) : (
<FileTree nodes={fileTree} depth={0} />
)}
</div>
</div>
);
}
function FileTree({ nodes, depth }: { nodes: FileNode[]; depth: number }) {
const { openPreview, previewPath, removeFile } = useStore();
const confirmDelete = (n: FileNode) => {
const msg =
n.type === "dir"
? `Supprimer le dossier ${n.path} et tout son contenu ?`
: `Supprimer ${n.path} ?`;
if (window.confirm(msg)) void removeFile(n.path);
};
return (
<>
{nodes.map((n) => {
const active = n.type === "file" && n.path === previewPath;
return (
<div key={n.path}>
<div
onClick={() => n.type === "file" && openPreview(n.path)}
className={`group mb-[3px] flex items-center gap-2 px-2 py-[5px] ${
n.type === "file"
? active
? "cursor-pointer border-2 border-line bg-card-deep text-white"
: "cursor-pointer text-ink-2 hover:bg-base"
: "text-muted"
}`}
style={{ paddingLeft: 8 + depth * 14 }}
>
{n.type === "dir" ? (
<span className="text-muted-2">▾</span>
) : (
<span
className={`h-2 w-2 border-2 ${
active ? "border-white bg-accent" : "border-line bg-muted-2"
}`}
/>
)}
<span className="min-w-0 flex-1 truncate">{n.name}</span>
{n.type === "file" && (
<button
onClick={(event) => {
event.stopPropagation();
downloadFile(n.path);
}}
className="hidden flex-none text-muted-2 hover:text-accent group-hover:block"
title={`Télécharger ${n.name}`}
>
<DownloadIcon size={13} />
</button>
)}
<button
onClick={(event) => {
event.stopPropagation();
confirmDelete(n);
}}
className="hidden flex-none text-muted-2 hover:text-warn group-hover:block"
title={`Supprimer ${n.name}`}
>
<TrashIcon size={13} />
</button>
</div>
{n.children && <FileTree nodes={n.children} depth={depth + 1} />}
</div>
);
})}
</>
);
}