Spaces:
Running
Running
| "use client"; | |
| import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |
| import type { | |
| AgentLogEntry, | |
| ChatMessage, | |
| ConnectionStatus, | |
| ResolvedChatContext, | |
| WorkPackage, | |
| } from "@/lib/work-package-types"; | |
| import type { LlmConfig } from "@/lib/llm-config"; | |
| import type { ModelTrace } from "@/lib/chat-trace"; | |
| import { parseCommand } from "@/lib/command-parser"; | |
| import { applyBoardAction, type ChatResponse } from "@/lib/board-actions"; | |
| import { runMockAgent } from "@/lib/mock-agent"; | |
| import { hydrateWorkPackages } from "@/lib/work-package-specs"; | |
| import { buildAutomationInstruction } from "@/lib/automation-agent"; | |
| import { extractProductTitle } from "@/lib/product-title"; | |
| import { resolveChatRequestContext } from "@/lib/chat-request-context"; | |
| import { | |
| buildResolvedContext, | |
| buildThinkingSummary, | |
| } from "@/lib/chat-request-context"; | |
| import { buildThinkingLogEntries } from "@/lib/agent-thinking-log"; | |
| import { AgentLogPanel } from "@/components/AgentLogPanel"; | |
| import { ChatPanel } from "@/components/ChatPanel"; | |
| import { WorkPackageBoard } from "@/components/WorkPackageBoard"; | |
| import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| import { | |
| isLlmConfigReady, | |
| LLM_SETTINGS_STORAGE_KEY, | |
| normalizeLlmConfig, | |
| } from "@/lib/llm-config"; | |
| import seedWorkPackages from "@/agentic_pm_demo_codex_plans/data/work-packages.seed.json"; | |
| type ViewMode = "chat" | "board"; | |
| function nowIso() { | |
| return new Date().toISOString(); | |
| } | |
| function newId(prefix: string) { | |
| return `${prefix}-${crypto.randomUUID()}`; | |
| } | |
| function welcomeMessage(live: boolean) { | |
| return live | |
| ? "Live mode is connected. Type a product idea to auto-run the full board, or use a work package command like:\n`@SRS ask ...` `@Design FMEA execute ...`" | |
| : "Mock mode is active until you add an API key in Model Settings below.\n\nType a product idea to auto-run the full board, or use a work package command like:\n`@SRS ask ...` `@Design FMEA execute ...`"; | |
| } | |
| export function AppShell() { | |
| const [view, setView] = useState<ViewMode>("chat"); | |
| const [projectTitle, setProjectTitle] = useState("Agentic PM Demo"); | |
| const [workPackages, setWorkPackages] = useState<WorkPackage[]>(() => | |
| hydrateWorkPackages(seedWorkPackages as WorkPackage[]), | |
| ); | |
| const [selectedWorkPackageId, setSelectedWorkPackageId] = useState< | |
| string | undefined | |
| >(() => (seedWorkPackages as WorkPackage[])[0]?.id); | |
| const [detailOpen, setDetailOpen] = useState(false); | |
| const [messages, setMessages] = useState<ChatMessage[]>(() => { | |
| return [ | |
| { | |
| id: "welcome-message", | |
| role: "assistant", | |
| content: welcomeMessage(false), | |
| createdAt: nowIso(), | |
| }, | |
| ]; | |
| }); | |
| const [agentLogs, setAgentLogs] = useState<AgentLogEntry[]>(() => [ | |
| { | |
| id: newId("log"), | |
| title: "Workspace ready", | |
| detail: | |
| "Workspace ready. Add an API key below for live automation, or stay in mock mode.", | |
| level: "info", | |
| createdAt: nowIso(), | |
| }, | |
| ]); | |
| const [draft, setDraft] = useState(""); | |
| const [busy, setBusy] = useState(false); | |
| const [settingsLoaded, setSettingsLoaded] = useState(false); | |
| const [connectionStatus, setConnectionStatus] = | |
| useState<ConnectionStatus>("idle"); | |
| const [connectionMessage, setConnectionMessage] = useState( | |
| "Add your model settings to enable live automation.", | |
| ); | |
| const [activeWorkPackageId, setActiveWorkPackageId] = useState< | |
| string | undefined | |
| >(); | |
| const [currentActivity, setCurrentActivity] = useState( | |
| "Ready for a product idea or a package command.", | |
| ); | |
| const [llmConfig, setLlmConfig] = useState<LlmConfig>(() => | |
| normalizeLlmConfig(), | |
| ); | |
| const lastConnectionLogKeyRef = useRef(""); | |
| const addLog = useCallback( | |
| (title: string, detail: string, level: AgentLogEntry["level"]) => { | |
| setAgentLogs((prev) => [ | |
| ...prev, | |
| { | |
| id: newId("log"), | |
| title, | |
| detail, | |
| level, | |
| createdAt: nowIso(), | |
| }, | |
| ]); | |
| }, | |
| [], | |
| ); | |
| const runConnectionCheck = useCallback(async (config: LlmConfig) => { | |
| if (!isLlmConfigReady(config)) { | |
| setConnectionStatus("idle"); | |
| setConnectionMessage("Add API key, base URL, and model to enable live automation."); | |
| return; | |
| } | |
| setConnectionStatus("checking"); | |
| setConnectionMessage("Testing model connection..."); | |
| try { | |
| const res = await fetch("/api/test-connection", { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify({ llmConfig: config }), | |
| }); | |
| const data = (await res.json()) as { | |
| ok?: boolean; | |
| status?: ConnectionStatus; | |
| message?: string; | |
| model?: string; | |
| }; | |
| if (data.ok) { | |
| setConnectionStatus("connected"); | |
| setConnectionMessage(data.message || "Connection verified."); | |
| const logKey = `${config.baseUrl}|${config.model}|connected`; | |
| if (lastConnectionLogKeyRef.current !== logKey) { | |
| lastConnectionLogKeyRef.current = logKey; | |
| addLog( | |
| "Model connected", | |
| `${config.model} is ready for live automation.`, | |
| "success", | |
| ); | |
| } | |
| return; | |
| } | |
| setConnectionStatus(data.status === "error" ? "error" : "idle"); | |
| setConnectionMessage(data.message || "Connection test did not complete."); | |
| const logKey = `${config.baseUrl}|${config.model}|${data.message}`; | |
| if (data.status === "error" && lastConnectionLogKeyRef.current !== logKey) { | |
| lastConnectionLogKeyRef.current = logKey; | |
| addLog( | |
| "Model connection failed", | |
| data.message || "Connection test failed.", | |
| "warn", | |
| ); | |
| } | |
| } catch (error) { | |
| setConnectionStatus("error"); | |
| setConnectionMessage(String(error)); | |
| } | |
| }, [addLog]); | |
| useEffect(() => { | |
| try { | |
| const raw = window.localStorage.getItem(LLM_SETTINGS_STORAGE_KEY); | |
| if (raw) { | |
| const parsed = JSON.parse(raw) as Partial<LlmConfig>; | |
| setLlmConfig(normalizeLlmConfig(parsed)); | |
| } | |
| } catch { | |
| // ignore malformed local storage | |
| } finally { | |
| setSettingsLoaded(true); | |
| } | |
| }, []); | |
| useEffect(() => { | |
| if (!settingsLoaded) return; | |
| try { | |
| window.localStorage.setItem( | |
| LLM_SETTINGS_STORAGE_KEY, | |
| JSON.stringify(llmConfig), | |
| ); | |
| } catch { | |
| // ignore persistence failures | |
| } | |
| }, [llmConfig, runConnectionCheck, settingsLoaded]); | |
| useEffect(() => { | |
| if (typeof document === "undefined") return; | |
| document.title = | |
| projectTitle === "Agentic PM Demo" | |
| ? projectTitle | |
| : `${projectTitle} · Agentic PM Demo`; | |
| }, [projectTitle]); | |
| useEffect(() => { | |
| setMessages((prev) => { | |
| if (!prev.length || prev[0]?.id !== "welcome-message") return prev; | |
| const next = [...prev]; | |
| next[0] = { | |
| ...next[0], | |
| content: welcomeMessage(connectionStatus === "connected"), | |
| }; | |
| return next; | |
| }); | |
| }, [connectionStatus]); | |
| useEffect(() => { | |
| if (!settingsLoaded) return; | |
| if (!isLlmConfigReady(llmConfig)) { | |
| setConnectionStatus("idle"); | |
| setConnectionMessage("Add API key, base URL, and model to enable live automation."); | |
| return; | |
| } | |
| const timeout = window.setTimeout(async () => { | |
| await runConnectionCheck(llmConfig); | |
| }, 700); | |
| return () => { | |
| window.clearTimeout(timeout); | |
| }; | |
| }, [llmConfig, runConnectionCheck, settingsLoaded]); | |
| const isMockMode = useMemo( | |
| () => connectionStatus !== "connected", | |
| [connectionStatus], | |
| ); | |
| function logModelTrace(trace: ModelTrace | undefined) { | |
| if (!trace) return; | |
| if (trace.requestPreview) { | |
| addLog( | |
| "Model request", | |
| `${trace.model ?? "unknown"}${trace.endpoint ? ` @ ${trace.endpoint}` : ""} :: ${trace.requestPreview}`, | |
| trace.provider === "live" ? "info" : "warn", | |
| ); | |
| } | |
| if (trace.responsePreview) { | |
| addLog( | |
| "Model response", | |
| trace.responsePreview, | |
| trace.provider === "live" ? "success" : "info", | |
| ); | |
| } | |
| } | |
| function logThinkingSummary(lines: string[] | undefined, knownLines: string[] = []) { | |
| const known = new Set(knownLines); | |
| for (const entry of buildThinkingLogEntries({ | |
| thinkingSummary: (lines ?? []).filter((line) => !known.has(line)), | |
| })) { | |
| addLog(entry.title, entry.detail, entry.level); | |
| } | |
| } | |
| async function requestChatResponse(args: { | |
| nextMessages: ChatMessage[]; | |
| boardSnapshot: WorkPackage[]; | |
| selectedId?: string; | |
| requestContext: ResolvedChatContext; | |
| parsedCommand: { | |
| referencedPackageName?: string; | |
| mode?: "ask" | "plan" | "change" | "execute"; | |
| instruction: string; | |
| }; | |
| }) { | |
| const res = await fetch("/api/chat", { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify({ | |
| messages: args.nextMessages.map((message) => ({ | |
| role: message.role, | |
| content: message.content, | |
| })), | |
| workPackages: args.boardSnapshot, | |
| selectedWorkPackageId: args.selectedId ?? null, | |
| requestContext: args.requestContext, | |
| parsedCommand: args.parsedCommand, | |
| llmConfig, | |
| }), | |
| }); | |
| if (!res.ok) { | |
| throw new Error(`Request failed with status ${res.status}.`); | |
| } | |
| return (await res.json()) as ChatResponse; | |
| } | |
| async function automateBoardFromProductIdea(args: { | |
| productIdea: string; | |
| nextMessages: ChatMessage[]; | |
| fallbackToMock?: boolean; | |
| }) { | |
| const hydrated = hydrateWorkPackages(workPackages, args.productIdea); | |
| let boardState = hydrated; | |
| const completedShortNames: string[] = []; | |
| const newTitle = extractProductTitle(args.productIdea); | |
| setProjectTitle(newTitle); | |
| setWorkPackages(hydrated); | |
| setDetailOpen(false); | |
| setActiveWorkPackageId(undefined); | |
| setCurrentActivity(`Preparing board for ${newTitle}.`); | |
| addLog( | |
| "Board hydrated", | |
| `Prepared ${hydrated.length} work packages from the product idea.`, | |
| "info", | |
| ); | |
| for (const workPackage of hydrated) { | |
| setActiveWorkPackageId(workPackage.id); | |
| setCurrentActivity(`Running ${workPackage.shortName} for ${newTitle}.`); | |
| addLog( | |
| "Package running", | |
| `${workPackage.shortName} is running with ${ | |
| isMockMode ? "mock mode" : llmConfig.model | |
| }.`, | |
| "info", | |
| ); | |
| const response = args.fallbackToMock | |
| ? runMockAgent({ | |
| messages: args.nextMessages, | |
| workPackages: boardState, | |
| selectedWorkPackageId: workPackage.id, | |
| parsedCommand: { | |
| referencedPackageName: workPackage.title, | |
| mode: "execute", | |
| instruction: buildAutomationInstruction(workPackage), | |
| }, | |
| }) | |
| : await requestChatResponse({ | |
| nextMessages: args.nextMessages, | |
| boardSnapshot: boardState, | |
| selectedId: workPackage.id, | |
| requestContext: buildResolvedContext({ | |
| parsedCommand: { | |
| referencedPackageName: workPackage.title, | |
| mode: "execute", | |
| instruction: buildAutomationInstruction(workPackage), | |
| }, | |
| shouldAutomateBoard: false, | |
| selectedWorkPackage: workPackage, | |
| provider: "live", | |
| }), | |
| parsedCommand: { | |
| referencedPackageName: workPackage.title, | |
| mode: "execute", | |
| instruction: buildAutomationInstruction(workPackage), | |
| }, | |
| }); | |
| if (response.boardAction) { | |
| boardState = applyBoardAction(boardState, response.boardAction); | |
| setWorkPackages(boardState); | |
| } | |
| logModelTrace(response.trace); | |
| completedShortNames.push(workPackage.shortName); | |
| addLog( | |
| "Package completed", | |
| `${workPackage.shortName} completed.`, | |
| "success", | |
| ); | |
| } | |
| setActiveWorkPackageId(undefined); | |
| setCurrentActivity(`Completed board automation for ${newTitle}.`); | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: newId("m"), | |
| role: "assistant", | |
| content: isMockMode | |
| ? `Completed all ${completedShortNames.length} work packages in mock mode.\n\nGenerated outputs: ${completedShortNames.join(", ")}` | |
| : `Completed all ${completedShortNames.length} work packages with ${llmConfig.model}.\n\nGenerated outputs: ${completedShortNames.join(", ")}`, | |
| createdAt: nowIso(), | |
| }, | |
| ]); | |
| addLog( | |
| "Board automation finished", | |
| `Generated outputs for ${completedShortNames.join(", ")}.`, | |
| "success", | |
| ); | |
| } | |
| async function handleChatSend(rawText: string) { | |
| const text = rawText.trim(); | |
| if (!text) return; | |
| const userMsg: ChatMessage = { | |
| id: newId("m"), | |
| role: "user", | |
| content: text, | |
| createdAt: nowIso(), | |
| }; | |
| const nextMessages = [...messages, userMsg]; | |
| const parsed = parseCommand(text, workPackages); | |
| const selectedWorkPackage = workPackages.find( | |
| (workPackage) => workPackage.id === selectedWorkPackageId, | |
| ); | |
| const contextWorkPackage = | |
| workPackages.find( | |
| (workPackage) => workPackage.id === parsed.matchedWorkPackageId, | |
| ) ?? selectedWorkPackage; | |
| const resolvedContext = resolveChatRequestContext({ | |
| parsed: parsed.parsed, | |
| detailOpen, | |
| selectedWorkPackage: contextWorkPackage, | |
| }); | |
| const effectiveParsedCommand = resolvedContext.parsedCommand; | |
| const requestContext = buildResolvedContext({ | |
| parsedCommand: effectiveParsedCommand, | |
| shouldAutomateBoard: resolvedContext.shouldAutomateBoard, | |
| selectedWorkPackage: contextWorkPackage, | |
| provider: isMockMode ? "mock" : "live", | |
| }); | |
| const requestThinking = buildThinkingSummary({ | |
| context: requestContext, | |
| model: isMockMode ? undefined : llmConfig.model, | |
| }); | |
| setMessages(nextMessages); | |
| setDraft(""); | |
| if (parsed.matchedWorkPackageId) { | |
| setSelectedWorkPackageId(parsed.matchedWorkPackageId); | |
| setDetailOpen(true); | |
| addLog( | |
| "Package selected", | |
| `Focused ${parsed.matchedWorkPackageTitle ?? parsed.parsed.referencedPackageName ?? "package"} from command context.`, | |
| "info", | |
| ); | |
| } else if (resolvedContext.source === "board-automation") { | |
| setDetailOpen(false); | |
| } | |
| if (parsed.error) { | |
| const suggestionLine = | |
| parsed.suggestions && parsed.suggestions.length | |
| ? `\n\nDid you mean:\n${parsed.suggestions | |
| .map((s) => `- ${s.shortName} (${s.title})`) | |
| .join("\n")}` | |
| : ""; | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: newId("m"), | |
| role: "assistant", | |
| content: `${parsed.error}${suggestionLine}`, | |
| createdAt: nowIso(), | |
| }, | |
| ]); | |
| addLog("Command parse warning", parsed.error, "warn"); | |
| return; | |
| } | |
| // Mock-first: call local mock agent if the command is @... OR if the user is just brainstorming. | |
| // Once the /api/chat route is present, we’ll prefer it for consistent behavior. | |
| setBusy(true); | |
| setCurrentActivity( | |
| effectiveParsedCommand.mode | |
| ? `Running ${effectiveParsedCommand.mode} on ${effectiveParsedCommand.referencedPackageName ?? "selected package"}.` | |
| : "Processing product idea and preparing the board.", | |
| ); | |
| addLog( | |
| "Agent started", | |
| effectiveParsedCommand.mode | |
| ? `Running ${effectiveParsedCommand.mode} on ${effectiveParsedCommand.referencedPackageName ?? "board context"}.` | |
| : "Processing free-form product planning request.", | |
| "info", | |
| ); | |
| logThinkingSummary(requestThinking); | |
| if (resolvedContext.source === "selected-package-context") { | |
| addLog( | |
| "Detail question routed", | |
| `Treating the plain-text prompt as an ask request for ${contextWorkPackage?.title ?? "the selected work package"}.`, | |
| "info", | |
| ); | |
| } else if (resolvedContext.source === "slash-command") { | |
| addLog( | |
| "Slash command routed", | |
| `Applied /${effectiveParsedCommand.mode ?? "ask"} to ${contextWorkPackage?.title ?? "the selected work package"}.`, | |
| "info", | |
| ); | |
| } | |
| try { | |
| if (resolvedContext.shouldAutomateBoard) { | |
| await automateBoardFromProductIdea({ | |
| productIdea: text, | |
| nextMessages, | |
| }); | |
| return; | |
| } | |
| // Call server route first (it will fall back to mock mode automatically if no key). | |
| if (effectiveParsedCommand.mode === "execute") { | |
| setActiveWorkPackageId( | |
| parsed.matchedWorkPackageId ?? selectedWorkPackageId, | |
| ); | |
| } | |
| const response = await requestChatResponse({ | |
| nextMessages, | |
| boardSnapshot: workPackages, | |
| selectedId: parsed.matchedWorkPackageId ?? selectedWorkPackageId, | |
| requestContext, | |
| parsedCommand: effectiveParsedCommand, | |
| }); | |
| logThinkingSummary(response.thinkingSummary, requestThinking); | |
| logModelTrace(response.trace); | |
| if (response?.boardAction) { | |
| setWorkPackages((prev) => applyBoardAction(prev, response?.boardAction)); | |
| if (response.boardAction.type === "replace_all") { | |
| setDetailOpen(false); | |
| addLog( | |
| "Board refreshed", | |
| "Hydrated the work package board from the product idea.", | |
| "success", | |
| ); | |
| } else if (response.boardAction.type !== "none") { | |
| addLog( | |
| "Board updated", | |
| `Applied ${response.boardAction.type} to ${effectiveParsedCommand.referencedPackageName ?? "work package"}.`, | |
| "success", | |
| ); | |
| } | |
| } | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: newId("m"), | |
| role: "assistant", | |
| content: response?.assistantMessage ?? "No response.", | |
| createdAt: nowIso(), | |
| }, | |
| ]); | |
| addLog( | |
| "Agent finished", | |
| response?.assistantMessage ?? "No response.", | |
| "success", | |
| ); | |
| } catch (err) { | |
| if (resolvedContext.shouldAutomateBoard) { | |
| addLog( | |
| "Automation fallback", | |
| "Live automation failed, continuing in mock mode.", | |
| "warn", | |
| ); | |
| await automateBoardFromProductIdea({ | |
| productIdea: text, | |
| nextMessages, | |
| fallbackToMock: true, | |
| }); | |
| return; | |
| } | |
| const fallbackResponse = runMockAgent({ | |
| messages: nextMessages, | |
| workPackages, | |
| selectedWorkPackageId: parsed.matchedWorkPackageId ?? selectedWorkPackageId, | |
| parsedCommand: effectiveParsedCommand, | |
| }); | |
| if (fallbackResponse.boardAction) { | |
| setWorkPackages((prev) => | |
| applyBoardAction(prev, fallbackResponse.boardAction), | |
| ); | |
| } | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: newId("m"), | |
| role: "assistant", | |
| content: | |
| fallbackResponse.assistantMessage || | |
| `Network error. (Mock mode)\n\n${String(err)}`, | |
| createdAt: nowIso(), | |
| }, | |
| ]); | |
| addLog( | |
| "Fallback path", | |
| "Live request failed locally; switched to the mock response path.", | |
| "warn", | |
| ); | |
| addLog("Agent error", String(err), "error"); | |
| } finally { | |
| setActiveWorkPackageId(undefined); | |
| setCurrentActivity("Ready for the next step."); | |
| setBusy(false); | |
| } | |
| } | |
| return ( | |
| <div className="flex h-[100dvh] flex-col bg-muted/40"> | |
| <div className="bg-background/72 backdrop-blur-sm"> | |
| <div className="mx-auto w-full max-w-[1760px] px-4 py-3 md:px-5"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <div className="min-w-0"> | |
| <div className="truncate text-sm font-semibold"> | |
| {projectTitle} | |
| </div> | |
| <div className="truncate text-xs text-muted-foreground"> | |
| {busy ? currentActivity : "Chat + automated IoT work packages"} | |
| </div> | |
| </div> | |
| <div className="shrink-0 md:hidden"> | |
| <Tabs value={view} onValueChange={(v) => setView(v as ViewMode)}> | |
| <TabsList> | |
| <TabsTrigger value="chat">Chat</TabsTrigger> | |
| <TabsTrigger value="board">Board</TabsTrigger> | |
| </TabsList> | |
| </Tabs> | |
| </div> | |
| <div className="hidden text-xs text-muted-foreground md:block"> | |
| {isMockMode ? "Mock mode" : `Live · ${llmConfig.model}`} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-hidden px-3 pb-3 md:px-4 md:pb-4"> | |
| <div className="mx-auto grid h-full w-full max-w-[1760px] grid-cols-1 gap-3 md:grid-cols-[minmax(320px,1fr)_minmax(0,2fr)]"> | |
| <div | |
| className={[ | |
| "h-full min-h-0 overflow-hidden rounded-[22px] bg-background/82 shadow-sm backdrop-blur-sm", | |
| view === "board" ? "hidden md:block" : "block", | |
| ].join(" ")} | |
| > | |
| <ChatPanel | |
| messages={messages} | |
| draft={draft} | |
| setDraft={setDraft} | |
| busy={busy} | |
| productTitle={projectTitle} | |
| currentActivity={currentActivity} | |
| workPackages={workPackages} | |
| selectedWorkPackageId={selectedWorkPackageId} | |
| onSelectWorkPackage={(id) => { | |
| setSelectedWorkPackageId(id); | |
| }} | |
| llmConfig={llmConfig} | |
| setLlmConfig={setLlmConfig} | |
| isMockMode={isMockMode} | |
| connectionStatus={connectionStatus} | |
| connectionMessage={connectionMessage} | |
| canTestConnection={isLlmConfigReady(llmConfig)} | |
| onTestConnection={() => { | |
| addLog("Connection test requested", "Checking the configured model settings.", "info"); | |
| void runConnectionCheck(llmConfig); | |
| }} | |
| onSend={handleChatSend} | |
| /> | |
| </div> | |
| <div | |
| className={[ | |
| "h-full min-h-0 overflow-hidden rounded-[22px] bg-background/82 shadow-sm backdrop-blur-sm", | |
| view === "chat" ? "hidden md:block" : "block", | |
| ].join(" ")} | |
| > | |
| <div className="flex h-full min-h-0 flex-col"> | |
| <div className="min-h-0 flex-1 p-2"> | |
| <WorkPackageBoard | |
| workPackages={workPackages} | |
| activeWorkPackageId={activeWorkPackageId} | |
| selectedWorkPackageId={selectedWorkPackageId} | |
| onSelect={(id) => { | |
| setSelectedWorkPackageId(id); | |
| setDetailOpen(true); | |
| const selected = workPackages.find((wp) => wp.id === id); | |
| addLog( | |
| "Detail opened", | |
| `Opened ${selected?.title ?? "work package"} detail view.`, | |
| "info", | |
| ); | |
| }} | |
| onPrefill={(s) => { | |
| setView("chat"); | |
| setDraft(s); | |
| addLog("Command prepared", `Prefilled chat input with: ${s}`, "info"); | |
| }} | |
| detailOpen={detailOpen} | |
| onBackToBoard={() => { | |
| setDetailOpen(false); | |
| addLog("Board view restored", "Returned to work package card board.", "info"); | |
| }} | |
| /> | |
| </div> | |
| <div className="h-[22dvh] min-h-[150px] px-2 pb-2"> | |
| <AgentLogPanel logs={agentLogs} busy={busy} /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |