mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Phase 4 : boucle agentique, outils fichiers et aperçu live
Backend : - tools.py : read_file / write_file / list_dir confinés au workspace (anti-traversal) - agent.py : boucle de tool-calling itérative au-dessus d'Ollama, génère des événements (token, tool_call, tool_result, final) - routes/chat.py : pilote run_agent, relaie les événements en SSE, persiste la réponse finale + le récap des outils (meta) - routes/files.py : arborescence et contenu du workspace (confiné) - db.py : colonne meta sur messages (+ migration douce), list_messages_for_model Frontend : - ToolCard : carte d'appel d'outil dans le fil (statut en cours/terminé/échec) - streamChat : événements tool_call / tool_result - store : streamTools, fileTree, preview (ouverture auto d'un HTML écrit) - LeftPanel : arborescence réelle du workspace, clic -> aperçu - PreviewPanel : rendu HTML en iframe + onglet Code - ChatPanel : rendu des outils dans les bulles agent (streaming + persisté) Tests manuels : outils + confinement, boucle agent (mock Ollama), chat HTTP de bout en bout (SSE, persistance meta, fichiers écrits, confinement route). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
1 parent
48cbcaafaf
commit
b6bf4edf97
13 files changed
+677
-80
No files matched your search
@@ -1,17 +1,25 @@
|
||||
/** Panneau droit : onglets Aperçu / Code / Logs + zone d'aperçu. */
|
||||
import { useState } from "react";
|
||||
import { useStore } from "../store/useStore";
|
||||
|
||||
type TabId = "preview" | "code";
|
||||
|
||||
/** Panneau droit : onglets Aperçu / Code + rendu du fichier sélectionné. */
|
||||
export function PreviewPanel() {
|
||||
const { previewPath, previewContent } = useStore();
|
||||
const [tab, setTab] = useState<TabId>("preview");
|
||||
|
||||
const isHtml = previewPath ? /\.html?$/.test(previewPath) : false;
|
||||
|
||||
return (
|
||||
<div className="flex w-[452px] flex-none flex-col border-l border-line-soft 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>Aperçu</Tab>
|
||||
<Tab>Code</Tab>
|
||||
<Tab>
|
||||
Logs
|
||||
<span className="ml-1.5 rounded-[5px] bg-line-strong px-1.5 py-px font-mono text-[10px] text-muted">
|
||||
0
|
||||
</span>
|
||||
<Tab active={tab === "preview"} onClick={() => setTab("preview")}>
|
||||
Aperçu
|
||||
</Tab>
|
||||
<Tab active={tab === "code"} onClick={() => setTab("code")}>
|
||||
Code
|
||||
</Tab>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,36 +27,61 @@ export function PreviewPanel() {
|
||||
{/* 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="font-mono text-[11.5px] text-muted-3">
|
||||
workspace/
|
||||
<span className="truncate font-mono text-[11.5px] text-muted-3">
|
||||
{previewPath ? `workspace/${previewPath}` : "workspace/"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Viewport */}
|
||||
<div className="scr mx-3.5 mb-3.5 flex-1 overflow-auto rounded-xl border border-line bg-[#faf6ef]">
|
||||
<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]">
|
||||
L'aperçu HTML s'affichera ici dès que l'agent générera une page.
|
||||
</div>
|
||||
</div>
|
||||
{!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"
|
||||
sandbox="allow-same-origin"
|
||||
/>
|
||||
) : (
|
||||
<pre className="m-0 whitespace-pre-wrap p-4 font-mono text-[11.5px] leading-relaxed text-[#2a2018]">
|
||||
{previewContent}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function Tab({
|
||||
children,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex h-7 items-center rounded-lg px-[13px] text-[12.5px] ${
|
||||
active
|
||||
? "bg-[#252019] font-semibold text-ink"
|
||||
@@ -56,6 +89,6 @@ function Tab({
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user