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:
MichaelandClaude Opus 4.8 committed 2026-06-30 14:21:05 +02:00
1 parent e8e6a1446c
commit b4fc8cee3d
16 files changed
+488 -419

No files matched your search

+20 -32
View File
@@ -15,7 +15,14 @@ const items: { id: View; icon: React.ReactNode }[] = [
{ id: "tools", icon: <NodesIcon /> },
];
/** Barre d'activité verticale (56px) à gauche. */
const btn = (on: boolean) =>
`flex h-10 w-10 items-center justify-center border-[3px] ${
on
? "border-white bg-accent text-white shadow-accent-soft"
: "border-chrome-3 text-on-dark-2 hover:text-white"
}`;
/** Barre d'activité verticale (60px) sombre à gauche. */
export function ActivityBar({
active,
onChange,
@@ -24,46 +31,27 @@ export function ActivityBar({
onChange: (v: View) => void;
}) {
return (
<div className="flex w-14 flex-none flex-col items-center gap-1 border-r border-line-soft bg-sunken py-3">
{items.map((it) => {
const on = active === it.id;
return (
<button
key={it.id}
onClick={() => onChange(it.id)}
className={`relative flex h-10 w-10 items-center justify-center rounded-[10px] ${
on ? "text-accent" : "text-muted-3 hover:text-ink-3"
}`}
style={on ? { background: "rgba(240,161,92,.14)" } : undefined}
>
{on && (
<span className="absolute left-[-12px] top-[11px] h-[18px] w-[3px] rounded bg-accent" />
)}
{it.icon}
</button>
);
})}
<div className="flex w-[60px] flex-none flex-col items-center gap-2 border-r-[3px] border-line bg-bar py-3">
{items.map((it) => (
<button
key={it.id}
onClick={() => onChange(it.id)}
className={btn(active === it.id)}
>
{it.icon}
</button>
))}
<div className="flex-1" />
<button
onClick={() => onChange("settings")}
className={`relative flex h-10 w-10 items-center justify-center rounded-[10px] ${
active === "settings" ? "text-accent" : "text-muted-3 hover:text-ink-3"
}`}
style={
active === "settings"
? { background: "rgba(240,161,92,.14)" }
: undefined
}
className={btn(active === "settings")}
>
{active === "settings" && (
<span className="absolute left-[-12px] top-[11px] h-[18px] w-[3px] rounded bg-accent" />
)}
<SettingsIcon />
</button>
<div className="mt-1 flex h-[30px] w-[30px] items-center justify-center rounded-full bg-[#3a332b] text-xs font-semibold text-ink-2">
<div className="font-pixel flex h-[34px] w-[34px] items-center justify-center border-[3px] border-white bg-white text-[11px] text-line">
M
</div>
</div>
+11 -11
View File
@@ -98,20 +98,20 @@ export const CheckIcon = ({ size = 14, className }: Props) => (
</svg>
);
/** Logo Loki : carré dégradé + losange interne. */
export const LokiMark = ({ size = 26 }: { size?: number }) => (
/** Logo Loki : carré orange à bordure blanche + carré interne (néo-brutaliste). */
export const LokiMark = ({ size = 30 }: { size?: number }) => (
<div
className="flex items-center justify-center rounded-lg bg-accent-grad"
style={{ width: size, height: size }}
className="flex items-center justify-center bg-accent"
style={{
width: size,
height: size,
border: "3px solid #fff",
boxShadow: "3px 3px 0 #ff5436",
borderRadius: 7,
}}
>
<div
className="bg-base"
style={{
width: size * 0.3,
height: size * 0.3,
borderRadius: 2,
transform: "rotate(45deg)",
}}
style={{ width: size * 0.3, height: size * 0.3, background: "#fff" }}
/>
</div>
);
+14 -19
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { useStore } from "../store/useStore";
import { ChevronDown } from "./Icon";
/** Sélecteur de modèle Ollama (chip de la barre supérieure). */
/** Sélecteur de modèle Ollama (chip orange de la barre supérieure). */
export function ModelSelector() {
const { models, selectedModel, setSelectedModel } = useStore();
const [open, setOpen] = useState(false);
@@ -20,19 +20,18 @@ export function ModelSelector() {
<div className="relative" ref={ref}>
<button
onClick={() => setOpen((v) => !v)}
className="flex h-7 items-center gap-2 rounded-lg border border-line-strong bg-[#2a251f] px-[11px]"
className="flex h-[34px] items-center gap-2 border-[3px] border-white bg-accent px-3 text-white shadow-accent-soft"
style={{ borderRadius: 7 }}
>
<span className="h-1.5 w-1.5 rounded-full bg-accent" />
<span className="font-mono text-xs font-medium text-[#e7ddcd]">
{selectedModel || "—"}
</span>
<ChevronDown size={12} className="text-muted-2" />
<span className="h-2.5 w-2.5 border-2 border-line bg-white" />
<span className="text-[14px] leading-none">{selectedModel || "—"}</span>
<ChevronDown size={12} />
</button>
{open && (
<div className="absolute right-0 top-9 z-20 w-64 rounded-lg border border-line bg-card p-1 shadow-frame">
<div className="absolute right-0 top-11 z-20 w-64 border-[3px] border-line bg-card p-1 shadow-hard">
{models.length === 0 && (
<div className="px-3 py-2 text-xs text-muted-3">
<div className="px-3 py-2 text-xs text-muted-2">
Aucun modèle installé
</div>
)}
@@ -43,22 +42,18 @@ export function ModelSelector() {
setSelectedModel(m.name);
setOpen(false);
}}
className={`flex w-full items-center gap-2 rounded-md px-2 py-2 text-left hover:bg-[#252019] ${
m.name === selectedModel ? "bg-[#252019]" : ""
className={`flex w-full items-center gap-2 px-2 py-2 text-left hover:bg-base ${
m.name === selectedModel ? "bg-base" : ""
}`}
>
<span
className={`h-1.5 w-1.5 rounded-full ${
m.name === selectedModel ? "bg-accent" : "bg-muted-3"
className={`h-2 w-2 border-2 border-line ${
m.name === selectedModel ? "bg-accent" : "bg-card"
}`}
/>
<span className="flex-1 font-mono text-xs text-ink-2">
{m.name}
</span>
<span className="flex-1 text-xs text-ink-2">{m.name}</span>
{m.size_go > 0 && (
<span className="font-mono text-[10px] text-muted-3">
{m.size_go} Go
</span>
<span className="text-[10px] text-muted-2">{m.size_go} Go</span>
)}
</button>
))}
+13 -12
View File
@@ -18,30 +18,31 @@ export function ToolCard({ call }: { call: ToolCall }) {
? "(…)"
: "()";
const write = call.name === "write_file";
return (
<div className="mb-[9px] overflow-hidden rounded-[11px] border border-line bg-card-deep">
<div className="flex items-center gap-2.5 px-[13px] py-2.5">
<div className="mb-[11px] overflow-hidden border-[3px] border-line bg-card shadow-hard-sm" style={{ borderRadius: 7 }}>
<div className="flex items-center gap-2.5 px-3 py-2.5">
<span
className="flex h-6 w-6 items-center justify-center rounded-[7px] text-accent"
style={{ background: "rgba(240,161,92,.13)" }}
className={`flex h-[26px] w-[26px] items-center justify-center border-2 border-line ${
write ? "bg-accent text-white" : "bg-card-deep text-white"
}`}
>
<ToolGlyph name={call.name} />
</span>
<span className="font-mono text-[12.5px] font-semibold text-[#e7ddcd]">
{call.name}
</span>
<span className="font-mono text-xs text-muted-2">{argPreview}</span>
<span className="text-[14px] text-ink">{call.name}</span>
<span className="text-[13px] text-muted-3">{argPreview}</span>
<div className="flex-1" />
{running ? (
<span className="flex items-center gap-1.5 font-mono text-[11px] text-muted">
<span className="flex items-center gap-1.5 text-[13px] text-muted-2">
<span className="h-2.5 w-2.5 animate-spin rounded-full border-2 border-muted-3 border-t-accent" />
en cours
</span>
) : pending ? (
<span className="font-mono text-[11px] text-warn">⏸ à valider</span>
<span className="text-[13px] text-accent">⏸ à valider</span>
) : (
<span
className={`flex items-center gap-1.5 font-mono text-[11px] ${
className={`flex items-center gap-1.5 text-[13px] ${
error ? "text-warn" : "text-ok"
}`}
>
@@ -50,7 +51,7 @@ export function ToolCard({ call }: { call: ToolCall }) {
)}
</div>
{call.summary && !running && (
<div className="border-t border-line-soft px-[13px] py-2.5 font-mono text-[11.5px] text-muted-3">
<div className="border-t-2 border-line-soft px-3 py-2 text-[13px] text-muted-2">
→ {call.summary}
</div>
)}
+12 -21
View File
@@ -2,50 +2,41 @@ import { useStore } from "../store/useStore";
import { ChevronDown, LokiMark } from "./Icon";
import { ModelSelector } from "./ModelSelector";
/** Barre supérieure : logo, fil d'Ariane, statut Ollama, sélecteur de modèle. */
/** Barre supérieure sombre : logo, fil d'Ariane, statut Ollama, sélecteur. */
export function TopBar() {
const status = useStore((s) => s.status);
const connected = status?.connected ?? false;
return (
<div className="flex h-12 flex-none items-center gap-3.5 border-b border-line-strong bg-bar px-4">
<div className="flex h-[54px] flex-none items-center gap-3.5 border-b-[3px] border-line bg-bar px-4">
<div className="flex items-center gap-2.5">
<LokiMark />
<div className="text-[15px] font-bold tracking-tight">Loki</div>
<div className="pt-px font-mono text-[11px] text-muted-3">
agent local
</div>
<div className="font-pixel text-[14px] text-white">LOKI</div>
<div className="text-[13px] leading-none text-on-dark-2">agent local</div>
</div>
<div className="h-5 w-px bg-line-strong" />
<div className="h-6 w-[3px] bg-chrome-3" />
<div className="flex items-center gap-2 text-[13px] font-medium text-ink-2">
<div className="flex items-center gap-2 text-[13px] text-on-dark">
<span>Nouvelle session</span>
<ChevronDown className="text-muted-3" />
<ChevronDown className="text-on-dark-3" />
</div>
<div className="flex-1" />
{/* Statut Ollama */}
<div
className="flex h-7 items-center gap-2 rounded-lg border border-line-strong bg-card-soft px-[11px]"
className="flex h-8 items-center gap-2 border-[3px] border-chrome-3 bg-chrome-2 px-[11px]"
title={connected ? status?.host : status?.error}
>
<span
className={`h-[7px] w-[7px] rounded-full ${
className={`h-[11px] w-[11px] border-2 border-line ${
connected ? "bg-ok" : "bg-warn"
}`}
style={
connected
? { boxShadow: "0 0 0 3px rgba(134,199,154,.16)" }
: undefined
}
/>
<span className="text-xs font-medium text-ink-3">Ollama</span>
<span className="font-mono text-[11px] text-muted-3">
{connected
? status?.host.replace(/^https?:\/\//, "")
: "déconnecté"}
<span className="text-[13px] text-on-dark">Ollama</span>
<span className="text-[13px] text-on-dark-3">
{connected ? status?.host.replace(/^https?:\/\//, "") : "déconnecté"}
</span>
</div>