mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
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>
186 lines
6.2 KiB
TypeScript
186 lines
6.2 KiB
TypeScript
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>
|
||
);
|
||
})}
|
||
</>
|
||
);
|
||
}
|
||
|