mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 18:58:43 +02:00
Phase 1-2 : socle Loki, design system et connexion Ollama
- Scaffold full-stack : backend FastAPI + frontend React/Vite/TS/Tailwind - Design system fidèle au thème (palette ambre/café, JetBrains Mono, tokens Tailwind) - Layout 3 panneaux : activity bar, historique/fichiers, chat, aperçu - Connexion Ollama : statut live, liste des modèles, pull avec progression SSE, sélecteur de modèle - Vue Configuration (Frame 2) : modèles, génération, outils, invite système - Dockerfile multi-stage + docker-compose (Ollama externe par défaut, profil ollama optionnel) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
34 files changed
+4343
No files matched your search
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
ChatIcon,
|
||||
ClockIcon,
|
||||
FilesIcon,
|
||||
NodesIcon,
|
||||
SettingsIcon,
|
||||
} from "./Icon";
|
||||
|
||||
export type View = "chat" | "history" | "files" | "tools" | "settings";
|
||||
|
||||
const items: { id: View; icon: React.ReactNode }[] = [
|
||||
{ id: "chat", icon: <ChatIcon /> },
|
||||
{ id: "history", icon: <ClockIcon /> },
|
||||
{ id: "files", icon: <FilesIcon /> },
|
||||
{ id: "tools", icon: <NodesIcon /> },
|
||||
];
|
||||
|
||||
/** Barre d'activité verticale (56px) à gauche. */
|
||||
export function ActivityBar({
|
||||
active,
|
||||
onChange,
|
||||
}: {
|
||||
active: View;
|
||||
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-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
|
||||
}
|
||||
>
|
||||
{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">
|
||||
M
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/** Icônes SVG (style line, stroke) reprises du visuel. */
|
||||
type Props = { size?: number; className?: string };
|
||||
|
||||
const base = (size: number, className?: string) => ({
|
||||
width: size,
|
||||
height: size,
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
strokeWidth: 1.7,
|
||||
strokeLinecap: "round" as const,
|
||||
strokeLinejoin: "round" as const,
|
||||
className,
|
||||
});
|
||||
|
||||
export const ChatIcon = ({ size = 19, className }: Props) => (
|
||||
<svg {...base(size, className)}>
|
||||
<path d="M4 6h16v10H10l-4 3v-3H4z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ClockIcon = ({ size = 19, className }: Props) => (
|
||||
<svg {...base(size, className)}>
|
||||
<circle cx="12" cy="12" r="8.5" />
|
||||
<path d="M12 7.5V12l3 1.8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FilesIcon = ({ size = 19, className }: Props) => (
|
||||
<svg {...base(size, className)}>
|
||||
<path d="M4 7h6l2 2h8v9H4z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const NodesIcon = ({ size = 19, className }: Props) => (
|
||||
<svg {...base(size, className)}>
|
||||
<circle cx="7" cy="7" r="2.4" />
|
||||
<circle cx="17" cy="7" r="2.4" />
|
||||
<circle cx="7" cy="17" r="2.4" />
|
||||
<circle cx="17" cy="17" r="2.4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SettingsIcon = ({ size = 18, className }: Props) => (
|
||||
<svg {...base(size, className)}>
|
||||
<line x1="4" y1="8" x2="20" y2="8" />
|
||||
<line x1="4" y1="13" x2="20" y2="13" />
|
||||
<line x1="4" y1="18" x2="20" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ChevronDown = ({ size = 13, className }: Props) => (
|
||||
<svg {...base(size, className)} strokeWidth={2}>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PlusIcon = ({ size = 13, className }: Props) => (
|
||||
<svg {...base(size, className)} strokeWidth={2.2}>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const RefreshIcon = ({ size = 15, className }: Props) => (
|
||||
<svg {...base(size, className)}>
|
||||
<path d="M21 12a9 9 0 1 1-3-6.7M21 4v4h-4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SendIcon = ({ size = 15, className }: Props) => (
|
||||
<svg {...base(size, className)} strokeWidth={2.2}>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ClipIcon = ({ size = 16, className }: Props) => (
|
||||
<svg {...base(size, className)} strokeWidth={1.8}>
|
||||
<path d="M21 11.5 12 20a5 5 0 0 1-7-7l8.5-8.5a3.5 3.5 0 0 1 5 5L10 16" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DownloadIcon = ({ size = 14, className }: Props) => (
|
||||
<svg {...base(size, className)} strokeWidth={1.9}>
|
||||
<path d="M12 4v11m0 0 4-4m-4 4-4-4M5 20h14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/** Logo Loki : carré dégradé + losange interne. */
|
||||
export const LokiMark = ({ size = 26 }: { size?: number }) => (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-lg bg-accent-grad"
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<div
|
||||
className="bg-base"
|
||||
style={{
|
||||
width: size * 0.3,
|
||||
height: size * 0.3,
|
||||
borderRadius: 2,
|
||||
transform: "rotate(45deg)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
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). */
|
||||
export function ModelSelector() {
|
||||
const { models, selectedModel, setSelectedModel } = useStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onClick = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onClick);
|
||||
return () => document.removeEventListener("mousedown", onClick);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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]"
|
||||
>
|
||||
<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" />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 top-9 z-20 w-64 rounded-lg border border-line bg-card p-1 shadow-frame">
|
||||
{models.length === 0 && (
|
||||
<div className="px-3 py-2 text-xs text-muted-3">
|
||||
Aucun modèle installé
|
||||
</div>
|
||||
)}
|
||||
{models.map((m) => (
|
||||
<button
|
||||
key={m.name}
|
||||
onClick={() => {
|
||||
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]" : ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full ${
|
||||
m.name === selectedModel ? "bg-accent" : "bg-muted-3"
|
||||
}`}
|
||||
/>
|
||||
<span className="flex-1 font-mono 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>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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. */
|
||||
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 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>
|
||||
|
||||
<div className="h-5 w-px bg-line-strong" />
|
||||
|
||||
<div className="flex items-center gap-2 text-[13px] font-medium text-ink-2">
|
||||
<span>Nouvelle session</span>
|
||||
<ChevronDown className="text-muted-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]"
|
||||
title={connected ? status?.host : status?.error}
|
||||
>
|
||||
<span
|
||||
className={`h-[7px] w-[7px] rounded-full ${
|
||||
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>
|
||||
</div>
|
||||
|
||||
<ModelSelector />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user