Backend: - Add Tavily web search tool wrapper (tools/web_search.py) - Add PDF reader + ChromaDB vector store tool (tools/pdf_reader.py) - Bind tools to LLM calls via .bind_tools() in dynamic_graph_builder - Implement God Mode using LangGraph interrupt_before + MemorySaver - Add approve/reject/modify API endpoints for God Mode - Add PDF upload endpoint with ingestion pipeline - Add persistent run history (CouncilRun model + run_service + API) - Add Alembic migration for council_runs table - Enhance WebSocket to emit run_paused and run_resumed events - Add tests for tools, God Mode, and run history Frontend: - Add God Mode approval UI (GodModePanel component) - Add Auto-Pilot / God Mode toggle in Konferenzzimmer - Add functional PDF upload handler - Add Conditional Edge editor (EdgeSettingsPanel component) - Add edge click selection in ArchitectCanvas - Update Zustand store with edge selection and update actions - Update types for God Mode, execution modes, and WS events - Update API client with God Mode, PDF upload, and blueprint run endpoints - Update WebSocket hook for paused/resumed events - Add Vitest config and frontend tests (store, parser, types, API) https://claude.ai/code/session_017U6idFgaqnYTXzPxA7mxMv
256 lines
8.8 KiB
TypeScript
256 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback, useRef } from "react";
|
|
import { ReactFlowProvider } from "@xyflow/react";
|
|
import { Play, Square, Upload, Shield, Zap } from "lucide-react";
|
|
import { ArchitectCanvas } from "@/app/components/ArchitectCanvas";
|
|
import { GodModePanel } from "@/app/components/panels/GodModePanel";
|
|
import { useCouncilWebSocket, PauseInfo } from "@/app/hooks/useCouncilWebSocket";
|
|
import { useCouncilStore } from "@/app/store/council-store";
|
|
import { runApi, pdfApi } from "@/app/utils/api-client";
|
|
import { ExecutionMode, GodModeAction } from "@/app/types/council";
|
|
|
|
export default function KonferenzzimmerPage() {
|
|
const [topic, setTopic] = useState("");
|
|
const [runId, setRunId] = useState<string | null>(null);
|
|
const [result, setResult] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isRunning, setIsRunning] = useState(false);
|
|
const [executionMode, setExecutionMode] = useState<ExecutionMode>("auto-pilot");
|
|
const [pauseInfo, setPauseInfo] = useState<PauseInfo | null>(null);
|
|
const [isResuming, setIsResuming] = useState(false);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const setActiveRun = useCouncilStore((s) => s.setActiveRun);
|
|
const clearActiveNode = useCouncilStore((s) => s.clearActiveNode);
|
|
|
|
const onComplete = useCallback((res: string) => {
|
|
setResult(res);
|
|
setIsRunning(false);
|
|
setRunId(null);
|
|
setPauseInfo(null);
|
|
}, []);
|
|
|
|
const onError = useCallback((err: string) => {
|
|
setError(err);
|
|
setIsRunning(false);
|
|
setRunId(null);
|
|
setPauseInfo(null);
|
|
}, []);
|
|
|
|
const onPaused = useCallback((info: PauseInfo) => {
|
|
setPauseInfo(info);
|
|
setIsResuming(false);
|
|
}, []);
|
|
|
|
const onResumed = useCallback(() => {
|
|
setPauseInfo(null);
|
|
setIsResuming(false);
|
|
}, []);
|
|
|
|
useCouncilWebSocket({ run_id: runId, onComplete, onError, onPaused, onResumed });
|
|
|
|
const handleStart = async () => {
|
|
if (!topic.trim()) return;
|
|
setResult(null);
|
|
setError(null);
|
|
setIsRunning(true);
|
|
setPauseInfo(null);
|
|
clearActiveNode();
|
|
try {
|
|
const godMode = executionMode === "god-mode";
|
|
const run = await runApi.start(topic, godMode);
|
|
setActiveRun(run);
|
|
setRunId(run.run_id);
|
|
} catch (e) {
|
|
setError("Fehler beim Starten: " + (e as Error).message);
|
|
setIsRunning(false);
|
|
}
|
|
};
|
|
|
|
const handleStop = () => {
|
|
setRunId(null);
|
|
setIsRunning(false);
|
|
clearActiveNode();
|
|
setActiveRun(null);
|
|
setPauseInfo(null);
|
|
};
|
|
|
|
const handleGodModeAction = async (action: GodModeAction, modifiedDraft?: string) => {
|
|
if (!runId) return;
|
|
setIsResuming(true);
|
|
|
|
try {
|
|
const modified_state = modifiedDraft ? { current_draft: modifiedDraft } : undefined;
|
|
await runApi.approve(runId, action, modified_state);
|
|
|
|
if (action === "reject") {
|
|
setError("Vom Benutzer im God Mode abgelehnt.");
|
|
setIsRunning(false);
|
|
setRunId(null);
|
|
setPauseInfo(null);
|
|
}
|
|
} catch (e) {
|
|
setError("Fehler bei God Mode Aktion: " + (e as Error).message);
|
|
setIsResuming(false);
|
|
}
|
|
};
|
|
|
|
const handlePdfUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (!file) return;
|
|
try {
|
|
const res = await pdfApi.upload(file);
|
|
setTopic((prev) =>
|
|
prev
|
|
? `${prev}\n\n[PDF hochgeladen: ${res.filename} — ${res.chunks_ingested} Abschnitte]`
|
|
: `[PDF hochgeladen: ${res.filename} — ${res.chunks_ingested} Abschnitte]`
|
|
);
|
|
} catch (e) {
|
|
setError("PDF-Upload fehlgeschlagen: " + (e as Error).message);
|
|
}
|
|
// Reset the input
|
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* Control bar */}
|
|
<header className="flex items-center gap-3 px-4 py-2 border-b border-slate-200 bg-white flex-shrink-0">
|
|
<textarea
|
|
value={topic}
|
|
onChange={(e) => setTopic(e.target.value)}
|
|
placeholder="Thema oder Aufgabe eingeben..."
|
|
rows={1}
|
|
className="flex-1 rounded-lg border border-slate-200 px-3 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-indigo-300"
|
|
/>
|
|
|
|
{/* PDF upload */}
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept=".pdf"
|
|
onChange={handlePdfUpload}
|
|
className="hidden"
|
|
/>
|
|
<button
|
|
onClick={() => fileInputRef.current?.click()}
|
|
className="flex items-center gap-1.5 text-sm text-slate-600 border border-slate-200 px-3 py-1.5 rounded-lg hover:bg-slate-50 transition-colors"
|
|
>
|
|
<Upload size={14} />
|
|
PDF
|
|
</button>
|
|
|
|
{/* Execution mode toggle */}
|
|
<button
|
|
onClick={() =>
|
|
setExecutionMode((m) => (m === "auto-pilot" ? "god-mode" : "auto-pilot"))
|
|
}
|
|
disabled={isRunning}
|
|
className={[
|
|
"flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg transition-colors border",
|
|
executionMode === "god-mode"
|
|
? "bg-amber-50 text-amber-700 border-amber-300 hover:bg-amber-100"
|
|
: "bg-slate-50 text-slate-600 border-slate-200 hover:bg-slate-100",
|
|
isRunning ? "opacity-50 cursor-not-allowed" : "",
|
|
].join(" ")}
|
|
title={
|
|
executionMode === "god-mode"
|
|
? "God Mode: Pause vor jedem Agenten"
|
|
: "Auto-Pilot: Automatischer Durchlauf"
|
|
}
|
|
>
|
|
{executionMode === "god-mode" ? (
|
|
<Shield size={14} />
|
|
) : (
|
|
<Zap size={14} />
|
|
)}
|
|
{executionMode === "god-mode" ? "God Mode" : "Auto-Pilot"}
|
|
</button>
|
|
|
|
{/* Start / Stop */}
|
|
{!isRunning ? (
|
|
<button
|
|
onClick={handleStart}
|
|
disabled={!topic.trim()}
|
|
className="flex items-center gap-1.5 text-sm text-white bg-indigo-600 px-3 py-1.5 rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
>
|
|
<Play size={14} />
|
|
Start
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={handleStop}
|
|
className="flex items-center gap-1.5 text-sm text-white bg-red-500 px-3 py-1.5 rounded-lg hover:bg-red-600 transition-colors"
|
|
>
|
|
<Square size={14} />
|
|
Stopp
|
|
</button>
|
|
)}
|
|
</header>
|
|
|
|
{/* Canvas (read-only, agent nodes pulse when active) */}
|
|
<div className="flex flex-1 overflow-hidden">
|
|
<ReactFlowProvider>
|
|
<div className="flex-1 h-full relative">
|
|
<ArchitectCanvas />
|
|
{isRunning && !pauseInfo && (
|
|
<div className="absolute top-3 left-1/2 -translate-x-1/2 bg-indigo-600 text-white text-xs px-4 py-1.5 rounded-full shadow-lg animate-pulse pointer-events-none">
|
|
Rat läuft…
|
|
</div>
|
|
)}
|
|
{pauseInfo && (
|
|
<div className="absolute top-3 left-1/2 -translate-x-1/2 bg-amber-500 text-white text-xs px-4 py-1.5 rounded-full shadow-lg pointer-events-none">
|
|
Pausiert — Freigabe erforderlich
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ReactFlowProvider>
|
|
|
|
{/* Output panel */}
|
|
<aside className="w-80 flex-shrink-0 border-l border-slate-200 bg-white flex flex-col">
|
|
<div className="px-4 py-3 border-b border-slate-100">
|
|
<h2 className="text-sm font-semibold text-slate-700">Ergebnis</h2>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{/* God Mode approval panel */}
|
|
{pauseInfo && (
|
|
<GodModePanel
|
|
pauseInfo={pauseInfo}
|
|
onAction={handleGodModeAction}
|
|
isResuming={isResuming}
|
|
/>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="rounded-lg bg-red-50 border border-red-200 p-3 text-sm text-red-700">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{result && (
|
|
<div className="rounded-lg bg-slate-50 border border-slate-200 p-3 text-sm text-slate-700 whitespace-pre-wrap leading-relaxed">
|
|
{result}
|
|
</div>
|
|
)}
|
|
{!result && !error && !isRunning && (
|
|
<p className="text-sm text-slate-400 italic">
|
|
Noch kein Ergebnis. Starte den Rat mit einem Thema.
|
|
</p>
|
|
)}
|
|
{isRunning && !result && !pauseInfo && (
|
|
<div className="space-y-2">
|
|
{[1, 2, 3].map((i) => (
|
|
<div
|
|
key={i}
|
|
className="h-3 bg-slate-100 rounded animate-pulse"
|
|
style={{ width: `${70 + i * 10}%` }}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|