mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 05:58:13 +02:00
Theme neo-brutaliste + panneau raisonnement + repli sans outils
UI : reskin complet vers un theme neo-brutaliste clair (bordures noires 3px, ombres dures, accent orange, polices Press Start 2P + DM Mono). - tailwind.config / index.css / index.html : palette, polices, ombres, scrollbars - TopBar, ActivityBar, LeftPanel, ChatPanel, PreviewPanel, SettingsView, ToolCard, ModelSelector, Icon : restyle complet Chat : panneau « Raisonnement » repliable et redimensionnable affichant le thinking de l'agent en direct (diffuse depuis le backend, persiste dans meta). Robustesse Ollama : les echecs de generation de parseur d'outils (« Unable to generate parser for this template ») declenchent desormais le repli automatique en conversation simple sans outils, au lieu d'un 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e8e6a1446c
commit
b4fc8cee3d
16 files changed
+488
-419
No files matched your search
@@ -13,7 +13,7 @@ const TOOL_DESC: Record<string, string> = {
|
||||
};
|
||||
const SENSITIVE = new Set(["run_shell"]);
|
||||
|
||||
/** Vue Configuration (Frame 2) — Modèle & génération, outils, invite système. */
|
||||
/** Vue Configuration — Modèle & génération, outils, invite système. */
|
||||
export function SettingsView() {
|
||||
const {
|
||||
models,
|
||||
@@ -27,7 +27,6 @@ export function SettingsView() {
|
||||
updateConfig,
|
||||
} = useStore();
|
||||
|
||||
// Brouillon local édité, synchronisé depuis la config serveur.
|
||||
const [draft, setDraft] = useState<AgentConfig | null>(config);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
@@ -76,47 +75,50 @@ export function SettingsView() {
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="scr flex-1 overflow-auto px-[30px] py-[26px]">
|
||||
<div className="scr flex-1 overflow-auto bg-base px-[30px] py-[26px]">
|
||||
<div className="mx-auto max-w-[1000px]">
|
||||
{/* En-tête */}
|
||||
<div className="mb-[22px] flex items-end justify-between">
|
||||
<div>
|
||||
<div className="text-xl font-bold tracking-tight">
|
||||
Modèle & génération
|
||||
<div className="font-pixel text-[13px] text-ink">
|
||||
MODÈLE & GÉNÉRATION
|
||||
</div>
|
||||
<div className="mt-1 text-[13px] text-muted-2">
|
||||
<div className="mt-2 text-[14px] text-muted-2">
|
||||
Choisis le modèle local et règle son comportement.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5 font-mono text-[11.5px] text-ok">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-ok" />
|
||||
<div className="flex items-center gap-1.5 text-[13px] text-ok">
|
||||
<span className="h-2 w-2 border-2 border-line bg-ok" />
|
||||
{status?.connected ? "Ollama connecté" : "Ollama déconnecté"}
|
||||
</div>
|
||||
<button
|
||||
onClick={save}
|
||||
disabled={!dirty}
|
||||
className="flex h-[30px] items-center gap-1.5 rounded-lg bg-accent px-3.5 text-[12.5px] font-bold text-[#1f1b16] disabled:opacity-40"
|
||||
className="flex h-[34px] items-center gap-1.5 border-[3px] border-line bg-accent px-4 text-[14px] text-white shadow-accent-soft disabled:opacity-40"
|
||||
style={{ borderRadius: 7 }}
|
||||
>
|
||||
{saved ? "Enregistré ✓" : dirty ? "Enregistrer" : "À jour"}
|
||||
{saved ? "✓ ENREGISTRÉ" : dirty ? "ENREGISTRER" : "À JOUR"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!draft ? (
|
||||
<div className="py-20 text-center text-muted-3">
|
||||
<div className="py-20 text-center text-muted-2">
|
||||
Chargement de la configuration…
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 items-start gap-5">
|
||||
<div className="grid grid-cols-2 items-start gap-[18px]">
|
||||
{/* Modèle Ollama */}
|
||||
<div className="rounded-card border border-line bg-card p-[18px]">
|
||||
<Card>
|
||||
<div className="mb-3.5 flex items-center justify-between">
|
||||
<div className="text-sm font-bold">Modèle Ollama</div>
|
||||
<div className="font-pixel text-[11px] text-ink">
|
||||
MODÈLE OLLAMA
|
||||
</div>
|
||||
<button
|
||||
onClick={refreshModels}
|
||||
className="flex h-[26px] items-center gap-1.5 rounded-[7px] border border-line-strong bg-base px-2.5 text-[11px] text-muted"
|
||||
className="flex h-7 items-center gap-1.5 border-2 border-line bg-card-soft px-2.5 text-[13px] text-muted-2"
|
||||
>
|
||||
<RefreshIcon size={12} />
|
||||
Actualiser
|
||||
@@ -125,7 +127,7 @@ export function SettingsView() {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{models.length === 0 && (
|
||||
<div className="rounded-[11px] border border-line bg-base p-4 text-center text-xs text-muted-3">
|
||||
<div className="border-2 border-line bg-base p-4 text-center text-[13px] text-muted-2">
|
||||
Aucun modèle installé. Télécharge-en un ci-dessous.
|
||||
</div>
|
||||
)}
|
||||
@@ -135,33 +137,33 @@ export function SettingsView() {
|
||||
<button
|
||||
key={m.name}
|
||||
onClick={() => setSelectedModel(m.name)}
|
||||
className="flex items-center gap-3 rounded-[11px] border p-3 text-left"
|
||||
style={{
|
||||
background: on ? "rgba(240,161,92,.10)" : "#1a1714",
|
||||
borderColor: on ? "rgba(240,161,92,.40)" : "#2c2720",
|
||||
}}
|
||||
className={`flex items-center gap-3 border-[3px] border-line p-3 text-left ${
|
||||
on ? "bg-card-deep" : "bg-card-soft"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="flex h-[18px] w-[18px] items-center justify-center rounded-full border-2"
|
||||
style={{ borderColor: on ? "#f0a15c" : "#4a443b" }}
|
||||
className={`flex h-[18px] w-[18px] flex-none items-center justify-center border-[3px] ${
|
||||
on ? "border-accent" : "border-muted-3"
|
||||
}`}
|
||||
>
|
||||
{on && <span className="h-2 w-2 rounded-full bg-accent" />}
|
||||
{on && <span className="h-2 w-2 bg-accent" />}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-[13px] font-semibold text-ink">
|
||||
<span
|
||||
className={`text-[14px] ${on ? "text-white" : "text-ink"}`}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
{m.name === status?.default_model && (
|
||||
<span
|
||||
className="rounded-[5px] px-1.5 py-px text-[10px] font-bold text-accent"
|
||||
style={{ background: "rgba(240,161,92,.14)" }}
|
||||
>
|
||||
PAR DÉFAUT
|
||||
<span className="font-pixel border-2 border-white bg-accent px-1.5 py-0.5 text-[7px] text-white">
|
||||
DÉFAUT
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-[3px] font-mono text-[10.5px] text-muted-2">
|
||||
<div
|
||||
className={`mt-1 text-[13px] ${on ? "text-on-dark-2" : "text-muted-2"}`}
|
||||
>
|
||||
{[
|
||||
m.size_go ? `${m.size_go} Go` : null,
|
||||
m.quantization,
|
||||
@@ -177,21 +179,21 @@ export function SettingsView() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="my-4 h-px bg-line" />
|
||||
<div className="my-4 h-[3px] bg-line" />
|
||||
|
||||
<div className="mb-2.5 text-[11px] font-bold uppercase tracking-[.05em] text-label">
|
||||
Télécharger un modèle
|
||||
<div className="font-pixel mb-2.5 text-[10px] text-label">
|
||||
TÉLÉCHARGER
|
||||
</div>
|
||||
<div className="mb-3 flex gap-2">
|
||||
<input
|
||||
value={pullName}
|
||||
onChange={(e) => setPullName(e.target.value)}
|
||||
placeholder="gemma2:9b"
|
||||
className="h-[34px] flex-1 rounded-[9px] border border-line-strong bg-base px-3 font-mono text-xs text-ink outline-none placeholder:text-muted-3"
|
||||
className="h-9 flex-1 border-[3px] border-line bg-card-soft px-3 text-[13px] text-ink outline-none placeholder:text-muted-3"
|
||||
/>
|
||||
<button
|
||||
onClick={doPull}
|
||||
className="flex h-[34px] items-center gap-1.5 rounded-[9px] border border-line-strong bg-[#2a251f] px-3.5 text-[12.5px] font-semibold text-[#e7ddcd]"
|
||||
className="flex h-9 items-center gap-1.5 border-[3px] border-line bg-card-deep px-3.5 text-[13px] text-white"
|
||||
>
|
||||
<DownloadIcon />
|
||||
Récupérer
|
||||
@@ -199,32 +201,28 @@ export function SettingsView() {
|
||||
</div>
|
||||
|
||||
{pullStatus && (
|
||||
<div className="rounded-[10px] border border-line bg-base p-3">
|
||||
<div className="border-[3px] border-line bg-card-soft p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-mono text-xs text-ink-2">
|
||||
{pullName}
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-accent">
|
||||
<span className="text-[13px] text-ink-2">{pullName}</span>
|
||||
<span className="text-[13px] text-accent">
|
||||
{pullStatus} {pullPct ? `· ${pullPct}%` : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 overflow-hidden rounded bg-line">
|
||||
<div
|
||||
className="h-full rounded bg-accent-grad transition-all"
|
||||
<div className="flex h-3.5 gap-px border-2 border-line bg-card p-px">
|
||||
<span
|
||||
className="bg-accent transition-all"
|
||||
style={{ width: `${pullPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Génération + Outils */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="rounded-card border border-line bg-card p-[18px]">
|
||||
<div className="mb-1 text-sm font-bold">Génération</div>
|
||||
<div className="mb-4 text-[11.5px] text-muted-2">
|
||||
Profil enregistré pour <b>{selectedModel || "ce modèle"}</b>.
|
||||
Le contexte détermine directement la taille du cache KV.
|
||||
<div className="flex flex-col gap-[18px]">
|
||||
<Card>
|
||||
<div className="font-pixel mb-4 text-[11px] text-ink">
|
||||
GÉNÉRATION
|
||||
</div>
|
||||
|
||||
<Slider
|
||||
@@ -291,17 +289,16 @@ export function SettingsView() {
|
||||
onChange={(v) => set("num_batch", Math.round(v))}
|
||||
last
|
||||
/>
|
||||
<div className="mt-3 rounded-lg border border-line-soft bg-base px-3 py-2 text-[11px] leading-relaxed text-muted-2">
|
||||
<div className="mt-3 border-2 border-line bg-card-soft px-3 py-2 text-[12px] leading-relaxed text-muted-2">
|
||||
La précision KV (<code>f16</code>/<code>q8_0</code>) est un
|
||||
réglage global du serveur Ollama et nécessite son redémarrage.
|
||||
Les valeurs ci-dessus sont, elles, propres à chaque modèle.
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="rounded-card border border-line bg-card p-[18px]">
|
||||
<Card>
|
||||
<div className="mb-3.5 flex items-center justify-between">
|
||||
<div className="text-sm font-bold">Outils de l'agent</div>
|
||||
<span className="font-mono text-[11px] text-muted-2">
|
||||
<div className="font-pixel text-[11px] text-ink">OUTILS</div>
|
||||
<span className="text-[13px] text-muted-2">
|
||||
{activeTools} / {availableTools.length} actifs
|
||||
</span>
|
||||
</div>
|
||||
@@ -310,68 +307,47 @@ export function SettingsView() {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`flex items-center gap-3 py-[9px] ${
|
||||
i > 0 ? "border-t border-line-soft" : ""
|
||||
className={`flex items-center gap-3 py-2 ${
|
||||
i > 0 ? "border-t-2 border-line-soft" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="w-32 flex-none font-mono text-[12.5px] text-ink-2">
|
||||
<span
|
||||
className={`w-32 flex-none text-[14px] ${on ? "text-ink" : "text-muted-3"}`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<span className="flex-1 text-xs text-muted-2">
|
||||
<span className="flex-1 text-[13px] text-muted-2">
|
||||
{TOOL_DESC[name] ?? ""}
|
||||
{SENSITIVE.has(name) && (
|
||||
<span className="text-warn"> · sensible</span>
|
||||
<span className="text-accent"> · sensible</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
set("tools", { ...draft.tools, [name]: !on })
|
||||
}
|
||||
className="relative h-5 w-[34px] rounded-full transition-colors"
|
||||
style={{ background: on ? "#f0a15c" : "#3a342c" }}
|
||||
>
|
||||
<span
|
||||
className="absolute top-0.5 h-4 w-4 rounded-full transition-all"
|
||||
style={{
|
||||
background: on ? "#1f1b16" : "#6f675c",
|
||||
left: on ? 16 : 2,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
<Toggle on={on} onClick={() => set("tools", { ...draft.tools, [name]: !on })} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{draft.tools.run_shell && (
|
||||
<div className="mt-2 flex items-center gap-3 rounded-[10px] border border-line-soft bg-base px-3 py-2.5">
|
||||
<span className="flex-1 text-xs text-ink-3">
|
||||
<div className="mt-2 flex items-center gap-3 border-2 border-line bg-base px-3 py-2.5">
|
||||
<span className="flex-1 text-[13px] text-ink-2">
|
||||
Demander une validation avant chaque commande shell
|
||||
</span>
|
||||
<button
|
||||
<Toggle
|
||||
on={draft.confirm_shell}
|
||||
onClick={() => set("confirm_shell", !draft.confirm_shell)}
|
||||
className="relative h-5 w-[34px] rounded-full transition-colors"
|
||||
style={{
|
||||
background: draft.confirm_shell ? "#f0a15c" : "#3a342c",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="absolute top-0.5 h-4 w-4 rounded-full transition-all"
|
||||
style={{
|
||||
background: draft.confirm_shell ? "#1f1b16" : "#6f675c",
|
||||
left: draft.confirm_shell ? 16 : 2,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invite système */}
|
||||
<div className="mt-5 rounded-card border border-line bg-card p-[18px]">
|
||||
<Card className="mt-[18px]">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-sm font-bold">Invite système</div>
|
||||
<span className="font-mono text-[11px] text-muted-2">
|
||||
<div className="font-pixel text-[11px] text-ink">
|
||||
INVITE SYSTÈME
|
||||
</div>
|
||||
<span className="text-[13px] text-muted-2">
|
||||
{draft.system_prompt.length} caractères
|
||||
</span>
|
||||
</div>
|
||||
@@ -379,9 +355,9 @@ export function SettingsView() {
|
||||
value={draft.system_prompt}
|
||||
onChange={(e) => set("system_prompt", e.target.value)}
|
||||
rows={6}
|
||||
className="scr w-full resize-y rounded-[10px] border border-line bg-sunken p-4 font-mono text-[12.5px] leading-[1.7] text-ink-3 outline-none focus:border-line-strong"
|
||||
className="scr w-full resize-y border-[3px] border-line bg-card-deep p-4 text-[13px] leading-[1.6] text-on-dark outline-none"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -389,6 +365,40 @@ export function SettingsView() {
|
||||
);
|
||||
}
|
||||
|
||||
function Card({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`border-[3px] border-line bg-card p-4 shadow-hard-lg ${className}`}
|
||||
style={{ borderRadius: 9 }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Toggle({ on, onClick }: { on: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`relative h-5 w-[38px] flex-none ${
|
||||
on ? "bg-card-deep" : "border-2 border-line bg-base"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute h-3.5 w-3.5 ${
|
||||
on ? "right-0.5 top-0.5 bg-accent" : "left-0.5 top-[3px] bg-muted-3"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Slider({
|
||||
label,
|
||||
value,
|
||||
@@ -410,25 +420,20 @@ function Slider({
|
||||
}) {
|
||||
const pct = ((value - min) / (max - min)) * 100;
|
||||
return (
|
||||
<div className={last ? "" : "mb-4"}>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className={last ? "" : "mb-[15px]"}>
|
||||
<div className="mb-[7px] flex items-center justify-between">
|
||||
<span className="text-[13px] text-ink-2">{label}</span>
|
||||
<span className="font-mono text-xs font-semibold text-accent">
|
||||
{fmt(value)}
|
||||
</span>
|
||||
<span className="text-[13px] text-accent">{fmt(value)}</span>
|
||||
</div>
|
||||
<div className="relative h-4">
|
||||
{/* Piste + remplissage + pouce (visuel fidèle) */}
|
||||
<div className="absolute top-1/2 h-1.5 w-full -translate-y-1/2 rounded bg-line" />
|
||||
<div className="relative h-[14px] border-2 border-line bg-card">
|
||||
<div
|
||||
className="absolute top-1/2 h-1.5 -translate-y-1/2 rounded bg-accent"
|
||||
className="absolute left-0 top-0 bottom-0 bg-line"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-[3px] border-card bg-accent"
|
||||
className="absolute top-1/2 h-5 w-4 -translate-x-1/2 -translate-y-1/2 border-[3px] border-line bg-accent"
|
||||
style={{ left: `${pct}%` }}
|
||||
/>
|
||||
{/* Input réel transparent par-dessus */}
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
|
||||
Reference in new issue
Block a user