mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 05:28:15 +02:00
feat(ui): chip projet dans le composer + aperçu réductible
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e7461096b
commit
abb7d2ea1f
3 files changed
+156
No files matched your search
@@ -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 : 📁 <projet> + 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<string | null>(null);
|
||||||
|
const rootRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<div ref={rootRef} className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
className="flex h-8 items-center gap-1.5 border-2 border-line bg-base px-2.5 text-[12px] text-ink-2"
|
||||||
|
title="Projet de travail de cette session"
|
||||||
|
>
|
||||||
|
📁 <span className="max-w-[140px] truncate">{active ?? "workspace"}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className="absolute bottom-[calc(100%+6px)] left-0 z-30 w-[240px] border-[3px] border-line bg-card shadow-hard"
|
||||||
|
style={{ borderRadius: 7 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => choose(null)}
|
||||||
|
className={`block w-full px-3 py-2 text-left text-[13px] text-ink-2 hover:bg-base ${
|
||||||
|
active === null ? "bg-base font-bold" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
workspace (racine)
|
||||||
|
</button>
|
||||||
|
{projects.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.name}
|
||||||
|
onClick={() => choose(p.name)}
|
||||||
|
className={`block w-full border-t-2 border-line-soft px-3 py-2 text-left text-[13px] text-ink-2 hover:bg-base ${
|
||||||
|
active === p.name ? "bg-base font-bold" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📁 {p.name}
|
||||||
|
<span className="ml-1.5 text-[11px] text-muted-2">
|
||||||
|
{p.files} fichier{p.files > 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<div className="border-t-2 border-line-soft p-2">
|
||||||
|
{creating ? (
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={draft}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setCreating(true)}
|
||||||
|
className="w-full text-left text-[13px] font-bold text-accent"
|
||||||
|
>
|
||||||
|
+ Nouveau projet
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{error && <div className="mt-1 text-[11px] text-warn">{error}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { useStore } from "../store/useStore";
|
|||||||
import { ChevronDown, LokiMark, SendIcon } from "../components/Icon";
|
import { ChevronDown, LokiMark, SendIcon } from "../components/Icon";
|
||||||
import { ToolCard } from "../components/ToolCard";
|
import { ToolCard } from "../components/ToolCard";
|
||||||
import { MessageContent } from "../components/MessageContent";
|
import { MessageContent } from "../components/MessageContent";
|
||||||
|
import { ProjectChip } from "../components/ProjectChip";
|
||||||
import type { Message, ToolCall } from "../api/client";
|
import type { Message, ToolCall } from "../api/client";
|
||||||
|
|
||||||
/** Panneau central : barre de contexte, fil de conversation, composer. */
|
/** Panneau central : barre de contexte, fil de conversation, composer. */
|
||||||
@@ -136,6 +137,7 @@ export function ChatPanel() {
|
|||||||
/>
|
/>
|
||||||
<div className="mt-1.5 flex items-center gap-2">
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
<ModeSelector />
|
<ModeSelector />
|
||||||
|
<ProjectChip />
|
||||||
<div
|
<div
|
||||||
className="flex h-8 min-w-0 max-w-[180px] items-center gap-1.5 border-2 border-line px-2.5 text-[13px] text-ink-2"
|
className="flex h-8 min-w-0 max-w-[180px] items-center gap-1.5 border-2 border-line px-2.5 text-[13px] text-ink-2"
|
||||||
title={selectedModel || undefined}
|
title={selectedModel || undefined}
|
||||||
|
|||||||
@@ -44,12 +44,40 @@ export function PreviewPanel() {
|
|||||||
|
|
||||||
const isHtml = previewPath ? /\.html?$/.test(previewPath) : false;
|
const isHtml = previewPath ? /\.html?$/.test(previewPath) : false;
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState(
|
||||||
|
() => 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.
|
// Journal d'activité : tous les appels d'outils de la session + en cours.
|
||||||
const logs: ToolCall[] = [
|
const logs: ToolCall[] = [
|
||||||
...messages.flatMap((m) => m.meta?.tools ?? []),
|
...messages.flatMap((m) => m.meta?.tools ?? []),
|
||||||
...streamTools,
|
...streamTools,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Replié : barre fine, aucun contenu monté (pas d'iframe vivante).
|
||||||
|
if (collapsed) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-9 flex-none flex-col items-center border-l-[3px] border-line bg-panel pt-3">
|
||||||
|
<button
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
title="Déplier l'aperçu"
|
||||||
|
className="text-[15px] text-muted-2 hover:text-accent"
|
||||||
|
>
|
||||||
|
⇤
|
||||||
|
</button>
|
||||||
|
<div className="mt-3 rotate-90 whitespace-nowrap text-[10px] font-bold tracking-wide text-muted-3">
|
||||||
|
APERÇU
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex flex-none flex-col border-l-[3px] border-line bg-panel"
|
className="relative flex flex-none flex-col border-l-[3px] border-line bg-panel"
|
||||||
@@ -85,6 +113,14 @@ export function PreviewPanel() {
|
|||||||
<Tab active={tab === "git"} onClick={() => setTab("git")}>
|
<Tab active={tab === "git"} onClick={() => setTab("git")}>
|
||||||
Git
|
Git
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<button
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
title="Replier l'aperçu"
|
||||||
|
className="px-2 text-[15px] text-muted-2 hover:text-accent"
|
||||||
|
>
|
||||||
|
⇥
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* URL bar */}
|
{/* URL bar */}
|
||||||
|
|||||||
Reference in new issue
Block a user