mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +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
+1
-1
@@ -6,7 +6,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Press+Start+2P&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Loki — Agent local</title>
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface Message {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
model?: string;
|
||||
meta?: { tools?: ToolCall[]; stats?: MessageStats } | null;
|
||||
meta?: { tools?: ToolCall[]; stats?: MessageStats; thinking?: string } | null;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ export async function streamChat(
|
||||
body: { session_id: string; content: string; model?: string },
|
||||
handlers: {
|
||||
onToken: (t: string) => void;
|
||||
onThinking: (t: string) => void;
|
||||
onToolCall: (call: ToolCall) => void;
|
||||
onToolResult: (call: ToolCall) => void;
|
||||
onToolConfirm: (command: string) => void;
|
||||
@@ -215,6 +216,7 @@ export async function streamChat(
|
||||
try {
|
||||
const payload = JSON.parse(dataLines.join("\n"));
|
||||
if (event === "token") handlers.onToken(payload.content);
|
||||
else if (event === "thinking") handlers.onThinking(payload.content);
|
||||
else if (event === "status") handlers.onStatus(payload.message);
|
||||
else if (event === "notice") handlers.onNotice(payload.message);
|
||||
else if (event === "tool_call")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+15
-9
@@ -10,21 +10,27 @@ body,
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1a1714;
|
||||
color: #efe9df;
|
||||
font-family: "Hanken Grotesk", system-ui, sans-serif;
|
||||
background: #ececef;
|
||||
color: #18181b;
|
||||
font-family: "DM Mono", ui-monospace, monospace;
|
||||
letter-spacing: -0.01em;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Barres de défilement fidèles au visuel */
|
||||
/* Police pixel pour les libellés (utilitaire .font-pixel via Tailwind aussi) */
|
||||
.font-pixel {
|
||||
font-family: "Press Start 2P", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* Barres de défilement néo-brutalistes */
|
||||
.scr::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.scr::-webkit-scrollbar-thumb {
|
||||
background: #3a342c;
|
||||
border-radius: 6px;
|
||||
background: #18181b;
|
||||
border: 2px solid #ececef;
|
||||
}
|
||||
.scr::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: #e4e4e7;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useStore } from "../store/useStore";
|
||||
import { ClipIcon, LokiMark, SendIcon } from "../components/Icon";
|
||||
import { ChevronDown, ClipIcon, LokiMark, SendIcon } from "../components/Icon";
|
||||
import { ToolCard } from "../components/ToolCard";
|
||||
import { MessageContent } from "../components/MessageContent";
|
||||
import type { Message, ToolCall } from "../api/client";
|
||||
@@ -12,6 +12,7 @@ export function ChatPanel() {
|
||||
messages,
|
||||
streaming,
|
||||
streamContent,
|
||||
streamThinking,
|
||||
streamStatus,
|
||||
streamNotice,
|
||||
streamTools,
|
||||
@@ -33,7 +34,7 @@ export function ChatPanel() {
|
||||
// Auto-scroll vers le bas à chaque token / message.
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
|
||||
}, [messages, streamContent, streamTools]);
|
||||
}, [messages, streamContent, streamThinking, streamTools]);
|
||||
|
||||
const submit = () => {
|
||||
if (!draft.trim() || streaming) return;
|
||||
@@ -53,21 +54,18 @@ export function ChatPanel() {
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 flex-col bg-base">
|
||||
{/* Barre de contexte */}
|
||||
<div className="flex h-[42px] flex-none items-center gap-2 border-b border-line-soft px-[18px]">
|
||||
<Chip>Invite système</Chip>
|
||||
<div className="flex h-[46px] flex-none items-center gap-2 border-b-[3px] border-line bg-panel px-4">
|
||||
<Chip>⚑ Invite système</Chip>
|
||||
<Chip>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-ok" />
|
||||
{activeTools} outil{activeTools > 1 ? "s" : ""} actif
|
||||
{activeTools > 1 ? "s" : ""}
|
||||
<span className="h-2 w-2 border-2 border-line bg-ok" />
|
||||
{activeTools} outil{activeTools > 1 ? "s" : ""}
|
||||
</Chip>
|
||||
<Chip>
|
||||
Température{" "}
|
||||
<b className="font-semibold text-[#e7ddcd]">
|
||||
{config ? config.temperature.toFixed(2) : "—"}
|
||||
</b>
|
||||
Temp{" "}
|
||||
<b className="text-accent">{config ? config.temperature.toFixed(1) : "—"}</b>
|
||||
</Chip>
|
||||
<div className="flex-1" />
|
||||
<span className="font-mono text-[11px] text-muted-4">
|
||||
<span className="text-[13px] text-muted-3">
|
||||
{currentSessionId
|
||||
? `${messages.length} message${messages.length > 1 ? "s" : ""}`
|
||||
: "aucune session"}
|
||||
@@ -78,17 +76,15 @@ export function ChatPanel() {
|
||||
<div ref={scrollRef} className="scr flex-1 overflow-auto px-7 py-6">
|
||||
{empty ? (
|
||||
<div className="mx-auto flex max-w-[680px] flex-col items-center justify-center gap-4 pt-24 text-center">
|
||||
<LokiMark size={44} />
|
||||
<div className="text-lg font-semibold text-ink">
|
||||
Prêt à travailler.
|
||||
</div>
|
||||
<div className="max-w-[360px] text-sm leading-relaxed text-muted">
|
||||
Décris une tâche à l'agent. Les outils fichiers et l'aperçu en
|
||||
direct arrivent à la prochaine étape.
|
||||
<LokiMark size={48} />
|
||||
<div className="font-pixel text-[15px] text-ink">PRÊT À TRAVAILLER</div>
|
||||
<div className="max-w-[380px] text-[14px] leading-relaxed text-muted">
|
||||
Décris une tâche à l'agent. Outils fichiers et aperçu en direct
|
||||
disponibles.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto flex max-w-[680px] flex-col gap-[22px]">
|
||||
<div className="mx-auto flex max-w-[680px] flex-col gap-5">
|
||||
{messages.map((m) => (
|
||||
<Bubble key={m.id} msg={m} />
|
||||
))}
|
||||
@@ -106,6 +102,7 @@ export function ChatPanel() {
|
||||
pending
|
||||
pendingStatus={streamStatus}
|
||||
notice={streamNotice}
|
||||
thinking={streamThinking}
|
||||
/>
|
||||
)}
|
||||
{pendingShell && (
|
||||
@@ -120,35 +117,34 @@ export function ChatPanel() {
|
||||
</div>
|
||||
|
||||
{/* Composer */}
|
||||
<div className="flex-none border-t border-line-soft px-7 pb-[18px] pt-3.5">
|
||||
<div className="flex-none border-t-[3px] border-line bg-panel px-7 pb-[18px] pt-3.5">
|
||||
<div className="mx-auto max-w-[680px]">
|
||||
<div className="rounded-[14px] border border-line-strong bg-card-soft p-[13px]">
|
||||
<div className="border-[3px] border-line bg-card p-3 shadow-hard" style={{ borderRadius: 8 }}>
|
||||
<textarea
|
||||
rows={1}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Envoyer un message à l'agent…"
|
||||
className="min-h-[42px] w-full resize-none bg-transparent text-sm leading-relaxed text-ink outline-none placeholder:text-muted-3"
|
||||
className="min-h-[40px] w-full resize-none bg-transparent text-[14px] leading-relaxed text-ink outline-none placeholder:text-muted-3"
|
||||
/>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<button className="flex h-[30px] w-[30px] items-center justify-center rounded-lg border border-line-strong text-muted-2">
|
||||
<button className="flex h-8 w-8 items-center justify-center border-2 border-line text-ink-2">
|
||||
<ClipIcon />
|
||||
</button>
|
||||
<div className="flex h-[30px] items-center gap-1.5 rounded-lg border border-line-strong px-2.5 font-mono text-[11.5px] text-muted">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-accent" />
|
||||
<div className="flex h-8 items-center gap-1.5 border-2 border-line px-2.5 text-[13px] text-ink-2">
|
||||
<span className="h-2 w-2 border-2 border-line bg-accent" />
|
||||
{selectedModel || "—"}
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<span className="font-mono text-[10.5px] text-muted-4">
|
||||
⏎ envoyer · ⇧⏎ ligne
|
||||
</span>
|
||||
<span className="text-[13px] text-muted-3">⏎ envoyer · ⇧⏎ ligne</span>
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={streaming || !draft.trim()}
|
||||
className="flex h-[34px] items-center gap-1.5 rounded-[9px] bg-accent px-4 text-[13px] font-bold text-[#1f1b16] disabled:opacity-50"
|
||||
className="flex h-[38px] items-center gap-1.5 border-[3px] border-line bg-card-deep px-4 text-[14px] text-white shadow-hard-accent disabled:opacity-40"
|
||||
style={{ borderRadius: 7 }}
|
||||
>
|
||||
{streaming ? "…" : "Envoyer"}
|
||||
{streaming ? "…" : "ENVOYER"}
|
||||
{!streaming && <SendIcon />}
|
||||
</button>
|
||||
</div>
|
||||
@@ -164,11 +160,13 @@ function Bubble({
|
||||
pending,
|
||||
pendingStatus,
|
||||
notice,
|
||||
thinking,
|
||||
}: {
|
||||
msg: Message;
|
||||
pending?: boolean;
|
||||
pendingStatus?: string;
|
||||
notice?: string | null;
|
||||
thinking?: string;
|
||||
}) {
|
||||
const time = new Date(msg.created_at * 1000).toLocaleTimeString("fr-FR", {
|
||||
hour: "2-digit",
|
||||
@@ -178,15 +176,15 @@ function Bubble({
|
||||
if (msg.role === "user") {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-[30px] w-[30px] flex-none items-center justify-center rounded-[9px] bg-[#3a332b] text-xs font-semibold text-ink-2">
|
||||
<div className="font-pixel flex h-[34px] w-[34px] flex-none items-center justify-center border-[3px] border-line bg-card-deep text-[11px] text-white">
|
||||
M
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mb-1.5 flex items-baseline gap-2">
|
||||
<span className="text-[13px] font-semibold">Vous</span>
|
||||
<span className="font-mono text-[10.5px] text-muted-4">{time}</span>
|
||||
<span className="text-[14px] text-ink">VOUS</span>
|
||||
<span className="text-[13px] text-muted-3">{time}</span>
|
||||
</div>
|
||||
<div className="rounded-xl rounded-tl-[4px] border border-line bg-card-soft px-[15px] py-3 text-sm leading-relaxed text-ink-2 whitespace-pre-wrap">
|
||||
<div className="border-[3px] border-line bg-card px-[14px] py-3 text-[14px] leading-snug text-ink shadow-hard-sm whitespace-pre-wrap" style={{ borderRadius: 7 }}>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,30 +194,31 @@ function Bubble({
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-[30px] w-[30px] flex-none items-center justify-center rounded-[9px] bg-accent-grad">
|
||||
<div
|
||||
className="bg-base"
|
||||
style={{ width: 8, height: 8, borderRadius: 2, transform: "rotate(45deg)" }}
|
||||
/>
|
||||
<div className="flex h-[34px] w-[34px] flex-none items-center justify-center border-[3px] border-line bg-accent">
|
||||
<div style={{ width: 10, height: 10, background: "#fff" }} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-2 flex items-baseline gap-2">
|
||||
<span className="text-[13px] font-semibold">Agent</span>
|
||||
<span className="font-mono text-[10.5px] text-muted-4">
|
||||
<span className="text-[14px] text-ink">LOKI</span>
|
||||
<span className="text-[13px] text-muted-3">
|
||||
{msg.model ? `${msg.model} · ` : ""}
|
||||
{time}
|
||||
</span>
|
||||
</div>
|
||||
<ReasoningPanel
|
||||
text={thinking ?? msg.meta?.thinking ?? ""}
|
||||
live={!!pending}
|
||||
/>
|
||||
{(msg.meta?.tools ?? []).map((t: ToolCall, i: number) => (
|
||||
<ToolCard key={i} call={t} />
|
||||
))}
|
||||
{notice && (
|
||||
<div className="mb-2 rounded-lg border border-[#4a3a2a] bg-card-deep px-3 py-2 text-xs text-warn">
|
||||
<div className="mb-2 border-[3px] border-line bg-card px-3 py-2 text-[13px] text-warn">
|
||||
{notice}
|
||||
</div>
|
||||
)}
|
||||
{(msg.content || pending) && (
|
||||
<div className="text-sm leading-[1.65] text-ink-2">
|
||||
<div className="text-[14px] leading-[1.5] text-ink-2">
|
||||
{msg.content ? (
|
||||
<MessageContent text={msg.content} />
|
||||
) : (
|
||||
@@ -231,9 +230,9 @@ function Bubble({
|
||||
</div>
|
||||
)}
|
||||
{!pending && msg.meta?.stats && (
|
||||
<div className="mt-2 flex items-center gap-2.5 font-mono text-[10.5px] text-muted-4">
|
||||
<div className="mt-2 flex items-center gap-2.5 text-[12px] text-muted-3">
|
||||
{msg.meta.stats.tokens_per_sec != null && (
|
||||
<span className="text-muted-3">
|
||||
<span className="text-accent-2">
|
||||
{msg.meta.stats.tokens_per_sec} tok/s
|
||||
</span>
|
||||
)}
|
||||
@@ -248,6 +247,56 @@ function Bubble({
|
||||
);
|
||||
}
|
||||
|
||||
/** Panneau « Raisonnement » : repliable et redimensionnable (poignée en bas). */
|
||||
function ReasoningPanel({ text, live }: { text: string; live: boolean }) {
|
||||
const [open, setOpen] = useState(live);
|
||||
const bodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (live) setOpen(true);
|
||||
}, [live]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && live) {
|
||||
bodyRef.current?.scrollTo({ top: bodyRef.current.scrollHeight });
|
||||
}
|
||||
}, [text, open, live]);
|
||||
|
||||
if (!text) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-[11px] overflow-hidden border-[3px] border-line bg-card shadow-hard-sm" style={{ borderRadius: 7 }}>
|
||||
<button
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 text-left"
|
||||
>
|
||||
<ChevronDown
|
||||
size={12}
|
||||
className={`text-ink-2 transition-transform ${open ? "" : "-rotate-90"}`}
|
||||
/>
|
||||
<span className="text-[13px] font-medium text-ink">Raisonnement</span>
|
||||
{live && (
|
||||
<span className="flex items-center gap-1 text-[12px] text-accent">
|
||||
<span className="h-2 w-2 animate-pulse border-2 border-line bg-accent" />
|
||||
en cours…
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-auto text-[12px] text-muted-3">
|
||||
{open ? "réduire" : "afficher"}
|
||||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
ref={bodyRef}
|
||||
className="scr max-h-[200px] min-h-[60px] resize-y overflow-auto whitespace-pre-wrap border-t-2 border-line-soft bg-card-deep px-3 py-2 text-[12.5px] leading-relaxed text-on-dark-2"
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ShellConfirm({
|
||||
command,
|
||||
onApprove,
|
||||
@@ -258,27 +307,27 @@ function ShellConfirm({
|
||||
onReject: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="ml-[43px] overflow-hidden rounded-[11px] border border-[#4a3a2a] bg-card-deep">
|
||||
<div className="flex items-center gap-2 border-b border-line-soft px-[13px] py-2.5">
|
||||
<span className="font-mono text-[12.5px] font-semibold text-warn">
|
||||
run_shell
|
||||
</span>
|
||||
<span className="text-xs text-muted-2">· commande sensible à valider</span>
|
||||
<div className="ml-[46px] overflow-hidden border-[3px] border-line bg-card shadow-hard" style={{ borderRadius: 7 }}>
|
||||
<div className="flex items-center gap-2 border-b-2 border-line px-3 py-2.5">
|
||||
<span className="text-[14px] text-accent">run_shell</span>
|
||||
<span className="text-[13px] text-muted-2">· commande sensible à valider</span>
|
||||
</div>
|
||||
<div className="px-[13px] py-3">
|
||||
<pre className="m-0 mb-3 overflow-auto whitespace-pre-wrap rounded-lg bg-sunken px-3 py-2.5 font-mono text-[12px] text-ink-2">
|
||||
<div className="px-3 py-3">
|
||||
<pre className="m-0 mb-3 overflow-auto whitespace-pre-wrap border-2 border-line bg-card-deep px-3 py-2.5 text-[12.5px] text-on-dark">
|
||||
$ {command}
|
||||
</pre>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onApprove}
|
||||
className="flex h-[30px] items-center gap-1.5 rounded-lg bg-accent px-3.5 text-[12.5px] font-bold text-[#1f1b16]"
|
||||
className="flex h-[32px] items-center gap-1.5 border-[3px] border-line bg-card-deep px-3.5 text-[13px] text-white shadow-hard-accent"
|
||||
style={{ borderRadius: 7 }}
|
||||
>
|
||||
Approuver & exécuter
|
||||
</button>
|
||||
<button
|
||||
onClick={onReject}
|
||||
className="flex h-[30px] items-center rounded-lg border border-line-strong px-3.5 text-[12.5px] font-semibold text-ink-3"
|
||||
className="flex h-[32px] items-center border-[3px] border-line bg-card px-3.5 text-[13px] text-ink-2"
|
||||
style={{ borderRadius: 7 }}
|
||||
>
|
||||
Refuser
|
||||
</button>
|
||||
@@ -290,7 +339,7 @@ function ShellConfirm({
|
||||
|
||||
function Chip({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-[25px] items-center gap-1.5 rounded-[7px] border border-line-strong bg-card-soft px-[9px] text-[11.5px] text-ink-3">
|
||||
<div className="flex h-7 items-center gap-1.5 border-2 border-line bg-card px-2.5 text-[13px] text-ink-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,24 +22,22 @@ export function LeftPanel() {
|
||||
}, [refreshSessions, refreshFiles]);
|
||||
|
||||
return (
|
||||
<div className="flex w-[268px] flex-none flex-col border-r border-line-soft bg-panel">
|
||||
<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="text-[11px] font-bold uppercase tracking-[.07em] text-label">
|
||||
Historique
|
||||
</span>
|
||||
<span className="font-pixel text-[10px] text-label">HISTORIQUE</span>
|
||||
<button
|
||||
onClick={newSession}
|
||||
className="flex cursor-pointer items-center gap-1 text-xs font-semibold text-accent"
|
||||
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-0.5 overflow-auto px-2.5">
|
||||
<div className="scr flex max-h-[45%] flex-col gap-2 overflow-auto px-3 py-1">
|
||||
{sessions.length === 0 && (
|
||||
<div className="px-3 py-3 text-xs text-muted-3">
|
||||
<div className="px-1 py-2 text-xs text-muted-2">
|
||||
Aucune session. Lance une conversation ci-dessous.
|
||||
</div>
|
||||
)}
|
||||
@@ -49,17 +47,15 @@ export function LeftPanel() {
|
||||
<div
|
||||
key={s.id}
|
||||
onClick={() => openSession(s.id)}
|
||||
className={`group relative cursor-pointer rounded-[9px] px-3 py-[9px] ${
|
||||
active ? "bg-[#252019]" : "hover:bg-[#221e18]"
|
||||
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 }}
|
||||
>
|
||||
{active && (
|
||||
<div className="absolute bottom-[9px] left-0 top-[9px] w-[3px] rounded bg-accent" />
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className={`flex-1 truncate text-[13px] ${
|
||||
active ? "font-semibold text-ink" : "font-medium text-ink-3"
|
||||
className={`flex-1 truncate text-[14px] leading-tight ${
|
||||
active ? "text-white" : "text-ink-2"
|
||||
}`}
|
||||
>
|
||||
{s.title}
|
||||
@@ -69,13 +65,15 @@ export function LeftPanel() {
|
||||
e.stopPropagation();
|
||||
removeSession(s.id);
|
||||
}}
|
||||
className="hidden text-muted-3 hover:text-warn group-hover:block"
|
||||
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-[3px] font-mono text-[10.5px] text-muted-3">
|
||||
<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>
|
||||
@@ -84,19 +82,17 @@ export function LeftPanel() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mx-4 my-3.5 h-px bg-line-soft" />
|
||||
<div className="mx-0 my-4 h-[3px] bg-line" />
|
||||
|
||||
{/* Fichiers */}
|
||||
<div className="flex items-center justify-between px-4 pb-2">
|
||||
<span className="text-[11px] font-bold uppercase tracking-[.07em] text-label">
|
||||
Fichiers
|
||||
</span>
|
||||
<span className="font-mono text-[10.5px] text-muted-4">workspace/</span>
|
||||
<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-2.5 pb-3.5 font-mono text-xs">
|
||||
<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-3">
|
||||
<div className="px-2 py-4 text-center text-muted-2">
|
||||
Aucun fichier pour l'instant.
|
||||
<br />
|
||||
L'agent créera des fichiers ici.
|
||||
@@ -110,41 +106,39 @@ export function LeftPanel() {
|
||||
}
|
||||
|
||||
function FileTree({ nodes, depth }: { nodes: FileNode[]; depth: number }) {
|
||||
const openPreview = useStore((s) => s.openPreview);
|
||||
const { openPreview, previewPath } = useStore();
|
||||
return (
|
||||
<>
|
||||
{nodes.map((n) => (
|
||||
<div key={n.path}>
|
||||
<div
|
||||
onClick={() => n.type === "file" && openPreview(n.path)}
|
||||
className={`flex items-center gap-2 rounded-[7px] px-2 py-[5px] ${
|
||||
n.type === "file"
|
||||
? "cursor-pointer text-ink-2 hover:bg-[#221e18]"
|
||||
: "text-muted"
|
||||
}`}
|
||||
style={{ paddingLeft: 8 + depth * 14 }}
|
||||
>
|
||||
{n.type === "dir" ? (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
) : (
|
||||
<span className="h-1.5 w-1.5 rounded-[1.5px] bg-accent" />
|
||||
)}
|
||||
<span className="truncate">{n.name}</span>
|
||||
{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={`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="truncate">{n.name}</span>
|
||||
</div>
|
||||
{n.children && <FileTree nodes={n.children} depth={depth + 1} />}
|
||||
</div>
|
||||
{n.children && <FileTree nodes={n.children} depth={depth + 1} />}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,29 +18,28 @@ export function PreviewPanel() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex w-[452px] flex-none flex-col border-l border-line-soft bg-panel">
|
||||
<div className="flex w-[452px] flex-none flex-col border-l-[3px] border-line bg-panel">
|
||||
{/* Onglets */}
|
||||
<div className="flex h-[42px] flex-none items-center gap-1 border-b border-line-soft px-3.5 pr-2">
|
||||
<div className="flex gap-0.5">
|
||||
<Tab active={tab === "preview"} onClick={() => setTab("preview")}>
|
||||
Aperçu
|
||||
</Tab>
|
||||
<Tab active={tab === "code"} onClick={() => setTab("code")}>
|
||||
Code
|
||||
</Tab>
|
||||
<Tab active={tab === "logs"} onClick={() => setTab("logs")}>
|
||||
Logs
|
||||
<span className="ml-1.5 rounded-[5px] bg-line-strong px-1.5 py-px font-mono text-[10px] text-muted">
|
||||
{logs.length}
|
||||
</span>
|
||||
</Tab>
|
||||
</div>
|
||||
<div className="flex h-[46px] flex-none items-center gap-1.5 border-b-[3px] border-line bg-base px-3">
|
||||
<Tab active={tab === "preview"} onClick={() => setTab("preview")}>
|
||||
Aperçu
|
||||
</Tab>
|
||||
<Tab active={tab === "code"} onClick={() => setTab("code")}>
|
||||
Code
|
||||
</Tab>
|
||||
<Tab active={tab === "logs"} onClick={() => setTab("logs")}>
|
||||
Logs
|
||||
<span className="font-pixel ml-1.5 border-2 border-line bg-accent px-1 py-0.5 text-[8px] text-white">
|
||||
{logs.length}
|
||||
</span>
|
||||
</Tab>
|
||||
</div>
|
||||
|
||||
{/* URL bar */}
|
||||
<div className="flex flex-none items-center gap-2 px-3.5 py-[9px]">
|
||||
<div className="flex h-[30px] flex-1 items-center gap-2 rounded-lg border border-line bg-sunken px-[11px]">
|
||||
<span className="truncate font-mono text-[11.5px] text-muted-3">
|
||||
<div className="flex flex-none items-center gap-2 px-3 py-2.5">
|
||||
<div className="flex h-8 flex-1 items-center gap-2 border-[3px] border-line bg-card px-[11px]">
|
||||
<span className="text-[13px]">🔒</span>
|
||||
<span className="truncate text-[13px] text-muted-2">
|
||||
{previewPath ? `workspace/${previewPath}` : "workspace/"}
|
||||
</span>
|
||||
</div>
|
||||
@@ -48,25 +47,23 @@ export function PreviewPanel() {
|
||||
|
||||
{/* Onglet Logs (fond sombre) */}
|
||||
{tab === "logs" ? (
|
||||
<div className="scr mx-3.5 mb-3.5 flex-1 overflow-auto rounded-xl border border-line bg-sunken p-3 font-mono text-[11.5px]">
|
||||
<div className="scr mx-3 mb-3 flex-1 overflow-auto border-[3px] border-line bg-card-deep p-3 text-[12.5px]">
|
||||
{logs.length === 0 ? (
|
||||
<div className="py-10 text-center text-muted-3">
|
||||
<div className="py-10 text-center text-on-dark-3">
|
||||
Aucune activité d'outil pour cette session.
|
||||
</div>
|
||||
) : (
|
||||
logs.map((l, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 border-b border-line-soft py-[7px] last:border-0"
|
||||
className="flex items-center gap-2 border-b-2 border-chrome-2 py-[7px] last:border-0"
|
||||
>
|
||||
<span
|
||||
className={
|
||||
l.status === "error"
|
||||
? "text-warn"
|
||||
: l.status === "pending"
|
||||
? "text-warn"
|
||||
l.status === "error" || l.status === "pending"
|
||||
? "text-accent"
|
||||
: l.status === "running"
|
||||
? "text-muted"
|
||||
? "text-on-dark-2"
|
||||
: "text-ok"
|
||||
}
|
||||
>
|
||||
@@ -78,42 +75,42 @@ export function PreviewPanel() {
|
||||
? "…"
|
||||
: "✓"}
|
||||
</span>
|
||||
<span className="text-ink-2">{l.name}</span>
|
||||
<span className="flex-1 truncate text-muted-3">
|
||||
<span className="text-on-dark">{l.name}</span>
|
||||
<span className="flex-1 truncate text-on-dark-3">
|
||||
{(l.args?.path as string) ??
|
||||
(l.args?.query as string) ??
|
||||
(l.args?.command as string) ??
|
||||
""}
|
||||
</span>
|
||||
<span className="text-muted-3">{l.summary}</span>
|
||||
<span className="text-on-dark-3">{l.summary}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* Viewport Aperçu / Code (fond clair) */
|
||||
<div className="scr mx-3.5 mb-3.5 flex-1 overflow-auto rounded-xl border border-line bg-[#faf6ef]">
|
||||
<div className="scr mx-3 mb-3 flex-1 overflow-auto border-[3px] border-line bg-[#faf6ef] shadow-hard">
|
||||
{!previewPath ? (
|
||||
<Empty>
|
||||
L'aperçu s'affichera ici dès que l'agent générera un fichier (clique
|
||||
aussi un fichier à gauche).
|
||||
</Empty>
|
||||
) : tab === "code" ? (
|
||||
<pre className="m-0 whitespace-pre-wrap p-4 font-mono text-[11.5px] leading-relaxed text-[#2a2018]">
|
||||
{previewContent}
|
||||
</pre>
|
||||
) : isHtml ? (
|
||||
<iframe
|
||||
title="aperçu"
|
||||
srcDoc={previewContent}
|
||||
className="h-full w-full border-0 bg-white"
|
||||
// allow-scripts : sans ça, le JS de la page générée ne s'exécute pas
|
||||
// (animations, canvas…). On garde une origine opaque (pas
|
||||
// d'allow-same-origin) pour que le script ne puisse pas atteindre Loki.
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
<Empty>
|
||||
L'aperçu s'affichera ici dès que l'agent générera un fichier (clique
|
||||
aussi un fichier à gauche).
|
||||
</Empty>
|
||||
) : tab === "code" ? (
|
||||
<pre className="m-0 whitespace-pre-wrap p-4 text-[12px] leading-relaxed text-[#2a2018]">
|
||||
{previewContent}
|
||||
</pre>
|
||||
) : isHtml ? (
|
||||
<iframe
|
||||
title="aperçu"
|
||||
srcDoc={previewContent}
|
||||
className="h-full w-full border-0 bg-white"
|
||||
// allow-scripts : sans ça, le JS de la page générée ne s'exécute pas
|
||||
// (animations, canvas…). On garde une origine opaque (pas
|
||||
// d'allow-same-origin) pour que le script ne puisse pas atteindre Loki.
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
) : (
|
||||
<pre className="m-0 whitespace-pre-wrap p-4 font-mono text-[11.5px] leading-relaxed text-[#2a2018]">
|
||||
<pre className="m-0 whitespace-pre-wrap p-4 text-[12px] leading-relaxed text-[#2a2018]">
|
||||
{previewContent}
|
||||
</pre>
|
||||
)}
|
||||
@@ -126,8 +123,8 @@ export function PreviewPanel() {
|
||||
function Empty({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 p-8 text-center">
|
||||
<div className="font-mono text-[11px] text-[#a98b63]">[ aucun aperçu ]</div>
|
||||
<div className="max-w-[240px] text-xs text-[#8a7a66]">{children}</div>
|
||||
<div className="font-pixel text-[10px] text-[#a98b63]">AUCUN APERÇU</div>
|
||||
<div className="max-w-[240px] text-[13px] text-[#8a7a66]">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -144,10 +141,8 @@ function Tab({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex h-7 items-center rounded-lg px-[13px] text-[12.5px] ${
|
||||
active
|
||||
? "bg-[#252019] font-semibold text-ink"
|
||||
: "font-medium text-muted-2"
|
||||
className={`flex h-[30px] items-center border-[3px] border-line px-[13px] text-[13px] ${
|
||||
active ? "bg-card-deep text-white" : "bg-card text-muted"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -32,6 +32,7 @@ interface LokiState {
|
||||
messages: Message[];
|
||||
streaming: boolean;
|
||||
streamContent: string; // réponse de l'agent en cours de frappe
|
||||
streamThinking: string; // raisonnement de l'agent en cours
|
||||
streamStatus: string;
|
||||
streamNotice: string | null;
|
||||
streamTools: ToolCall[]; // appels d'outils de la réponse en cours
|
||||
@@ -73,6 +74,7 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
messages: [],
|
||||
streaming: false,
|
||||
streamContent: "",
|
||||
streamThinking: "",
|
||||
streamStatus: "",
|
||||
streamNotice: null,
|
||||
streamTools: [],
|
||||
@@ -250,6 +252,7 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
messages: [...get().messages, userMsg],
|
||||
streaming: true,
|
||||
streamContent: "",
|
||||
streamThinking: "",
|
||||
streamStatus: "Connexion à Ollama…",
|
||||
streamNotice: null,
|
||||
streamTools: [],
|
||||
@@ -260,6 +263,7 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
{ session_id: sid, content, model: get().selectedModel || undefined },
|
||||
{
|
||||
onToken: (t) => set({ streamContent: get().streamContent + t }),
|
||||
onThinking: (t) => set({ streamThinking: get().streamThinking + t }),
|
||||
onStatus: (message) => set({ streamStatus: message }),
|
||||
onNotice: (message) => set({ streamNotice: message }),
|
||||
onToolCall: (call) =>
|
||||
@@ -289,6 +293,7 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
set({
|
||||
streaming: false,
|
||||
streamContent: "",
|
||||
streamThinking: "",
|
||||
streamStatus: "",
|
||||
streamNotice: null,
|
||||
streamTools: [],
|
||||
@@ -310,6 +315,7 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
set({
|
||||
streaming: false,
|
||||
streamContent: "",
|
||||
streamThinking: "",
|
||||
streamStatus: "",
|
||||
streamNotice: null,
|
||||
streamTools: [],
|
||||
|
||||
+46
-33
@@ -1,8 +1,9 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
/**
|
||||
* Système de design « Loki » — extrait fidèlement du visuel Atelier_Agent.
|
||||
* Thème sombre chaleureux (café / ambre).
|
||||
* Système de design « Loki » — thème néo-brutaliste clair.
|
||||
* Bordures noires épaisses, ombres dures décalées, accent orange,
|
||||
* police pixel (Press Start 2P) pour les libellés, DM Mono pour le corps.
|
||||
*/
|
||||
export default {
|
||||
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||
@@ -10,48 +11,60 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
// Fonds
|
||||
base: "#1a1714",
|
||||
bar: "#1d1a15",
|
||||
panel: "#1c1814",
|
||||
sunken: "#161310",
|
||||
base: "#ececef", // contenu principal
|
||||
bar: "#18181b", // chrome sombre (barres)
|
||||
panel: "#fafafa", // panneaux latéraux
|
||||
sunken: "#18181b", // encarts sombres (code, invite système)
|
||||
// Surfaces / cartes
|
||||
card: "#211d18",
|
||||
"card-soft": "#221e18",
|
||||
"card-deep": "#1f1b16",
|
||||
card: "#ffffff",
|
||||
"card-soft": "#fafafa",
|
||||
"card-deep": "#18181b",
|
||||
// Bordures
|
||||
line: "#2c2720",
|
||||
"line-soft": "#262219",
|
||||
"line-strong": "#322d26",
|
||||
// Accent ambre
|
||||
accent: "#f0a15c",
|
||||
"accent-1": "#f4ad6c",
|
||||
"accent-2": "#df7c4c",
|
||||
line: "#18181b", // bordure noire signature
|
||||
"line-soft": "#e4e4e7", // séparateur clair
|
||||
"line-strong": "#18181b",
|
||||
// Accent orange
|
||||
accent: "#ff5436",
|
||||
"accent-1": "#ff6b50",
|
||||
"accent-2": "#e23e22",
|
||||
// Statuts
|
||||
ok: "#86c79a",
|
||||
info: "#5b8fb0",
|
||||
warn: "#c98b5b",
|
||||
// Textes
|
||||
ink: "#efe9df",
|
||||
"ink-2": "#cfc5b6",
|
||||
"ink-3": "#bdb3a4",
|
||||
muted: "#9a9082",
|
||||
"muted-2": "#8a8175",
|
||||
"muted-3": "#766d60",
|
||||
"muted-4": "#6a6256",
|
||||
label: "#7a7164",
|
||||
ok: "#22c55e",
|
||||
info: "#3b82f6",
|
||||
warn: "#dc2626",
|
||||
// Textes (sombres sur fond clair)
|
||||
ink: "#18181b",
|
||||
"ink-2": "#27272a",
|
||||
"ink-3": "#3f3f46",
|
||||
muted: "#52525b",
|
||||
"muted-2": "#71717a",
|
||||
"muted-3": "#a1a1aa",
|
||||
"muted-4": "#a1a1aa",
|
||||
label: "#52525b",
|
||||
// Teintes claires utiles sur le chrome sombre
|
||||
"chrome-2": "#27272a",
|
||||
"chrome-3": "#3f3f46",
|
||||
"on-dark": "#e4e4e7",
|
||||
"on-dark-2": "#a1a1aa",
|
||||
"on-dark-3": "#71717a",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Hanken Grotesk"', "system-ui", "sans-serif"],
|
||||
mono: ['"JetBrains Mono"', "ui-monospace", "monospace"],
|
||||
sans: ['"DM Mono"', "ui-monospace", "monospace"],
|
||||
mono: ['"DM Mono"', "ui-monospace", "monospace"],
|
||||
pixel: ['"Press Start 2P"', "ui-monospace", "monospace"],
|
||||
},
|
||||
borderRadius: {
|
||||
card: "14px",
|
||||
card: "9px",
|
||||
},
|
||||
boxShadow: {
|
||||
frame: "0 24px 60px rgba(0,0,0,.35)",
|
||||
"hard-sm": "3px 3px 0 #18181b",
|
||||
hard: "4px 4px 0 #18181b",
|
||||
"hard-lg": "7px 7px 0 #18181b",
|
||||
"hard-accent": "3px 3px 0 #ff5436",
|
||||
"accent-soft": "3px 3px 0 rgba(255,84,54,.4)",
|
||||
frame: "4px 4px 0 #18181b",
|
||||
},
|
||||
backgroundImage: {
|
||||
"accent-grad": "linear-gradient(140deg, #f4ad6c, #df7c4c)",
|
||||
"accent-grad": "linear-gradient(140deg, #ff6b50, #e23e22)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user