diff --git a/frontend/src/components/ProjectChip.tsx b/frontend/src/components/ProjectChip.tsx new file mode 100644 index 0000000..83cfbe4 --- /dev/null +++ b/frontend/src/components/ProjectChip.tsx @@ -0,0 +1,118 @@ +import { useEffect, useRef, useState } from "react"; +import { useStore } from "../store/useStore"; +import { createProject } from "../api/client"; + +/** Sélecteur de projet du composer : 📁 + menu (liste, création). */ +export function ProjectChip() { + const { projects, refreshProjects, currentProject, setProject } = useStore(); + const [open, setOpen] = useState(false); + const [creating, setCreating] = useState(false); + const [draft, setDraft] = useState(""); + const [error, setError] = useState(null); + const rootRef = useRef(null); + + const active = currentProject(); + + useEffect(() => { + if (open) void refreshProjects(); + }, [open, refreshProjects]); + + useEffect(() => { + if (!open) return; + const onDown = (e: MouseEvent) => { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) { + setOpen(false); + setCreating(false); + setError(null); + } + }; + document.addEventListener("mousedown", onDown); + return () => document.removeEventListener("mousedown", onDown); + }, [open]); + + const choose = async (name: string | null) => { + setOpen(false); + await setProject(name); + }; + + const create = async () => { + const name = draft.trim(); + if (!name) return; + try { + await createProject(name); + setCreating(false); + setDraft(""); + await choose(name); + } catch (err) { + setError(err instanceof Error ? err.message : "création impossible"); + } + }; + + return ( +
+ + + {open && ( +
+ + {projects.map((p) => ( + + ))} +
+ {creating ? ( + setDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void create(); + if (e.key === "Escape") { + setCreating(false); + setError(null); + } + }} + placeholder="nom-du-projet" + className="w-full border-2 border-line bg-base px-2 py-1 text-[12px] text-ink outline-none" + /> + ) : ( + + )} + {error &&
{error}
} +
+
+ )} +
+ ); +} diff --git a/frontend/src/panels/ChatPanel.tsx b/frontend/src/panels/ChatPanel.tsx index 3817267..5bcf9dd 100644 --- a/frontend/src/panels/ChatPanel.tsx +++ b/frontend/src/panels/ChatPanel.tsx @@ -3,6 +3,7 @@ import { useStore } from "../store/useStore"; import { ChevronDown, LokiMark, SendIcon } from "../components/Icon"; import { ToolCard } from "../components/ToolCard"; import { MessageContent } from "../components/MessageContent"; +import { ProjectChip } from "../components/ProjectChip"; import type { Message, ToolCall } from "../api/client"; /** Panneau central : barre de contexte, fil de conversation, composer. */ @@ -136,6 +137,7 @@ export function ChatPanel() { />
+
window.localStorage.getItem("loki.preview.collapsed") === "1" + ); + const toggleCollapsed = () => { + setCollapsed((c) => { + window.localStorage.setItem("loki.preview.collapsed", c ? "0" : "1"); + return !c; + }); + }; + // Journal d'activité : tous les appels d'outils de la session + en cours. const logs: ToolCall[] = [ ...messages.flatMap((m) => m.meta?.tools ?? []), ...streamTools, ]; + // Replié : barre fine, aucun contenu monté (pas d'iframe vivante). + if (collapsed) { + return ( +
+ +
+ APERÇU +
+
+ ); + } + return (
setTab("git")}> Git +
+
{/* URL bar */}