diff options
Diffstat (limited to 'makima/frontend/src/routes')
| -rw-r--r-- | makima/frontend/src/routes/contracts.tsx | 2 | ||||
| -rw-r--r-- | makima/frontend/src/routes/history.tsx | 325 |
2 files changed, 326 insertions, 1 deletions
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx index 8ed4ab5..cd385f9 100644 --- a/makima/frontend/src/routes/contracts.tsx +++ b/makima/frontend/src/routes/contracts.tsx @@ -612,7 +612,7 @@ function ContractsPageContent() { </span> </div> <div className="text-[10px] text-[#556677] truncate"> - {suggestion.repositoryUrl || suggestion.localPath} + {repoType === "local" ? suggestion.localPath : suggestion.repositoryUrl} </div> </button> ))} diff --git a/makima/frontend/src/routes/history.tsx b/makima/frontend/src/routes/history.tsx new file mode 100644 index 0000000..fc88f0e --- /dev/null +++ b/makima/frontend/src/routes/history.tsx @@ -0,0 +1,325 @@ +import { useState, useCallback, useEffect } from "react"; +import { useParams, useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { TimelineList } from "../components/history/TimelineList"; +import { ConversationView } from "../components/history/ConversationView"; +import { CheckpointList } from "../components/history/CheckpointList"; +import { HistoryFilters } from "../components/history/HistoryFilters"; +import { ResumeControls } from "../components/history/ResumeControls"; +import { useAuth } from "../contexts/AuthContext"; +import type { + HistoryEvent, + TaskConversationResponse, + SupervisorConversationResponse, + TaskCheckpoint, + ContractSummary, +} from "../lib/api"; +import { + getTimeline, + getTaskConversation, + getSupervisorConversation, + getTaskCheckpoints, + listContracts, +} from "../lib/api"; + +// Detail view modes +type DetailMode = "conversation" | "checkpoints"; + +export default function HistoryPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); + + // Timeline state + const [events, setEvents] = useState<HistoryEvent[]>([]); + const [totalCount, setTotalCount] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + + // Filters + const [contracts, setContracts] = useState<ContractSummary[]>([]); + const [selectedContractId, setSelectedContractId] = useState<string | null>(null); + const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null); + const [dateFrom, setDateFrom] = useState<string>(""); + const [dateTo, setDateTo] = useState<string>(""); + + // Selected event and detail + const [selectedEvent, setSelectedEvent] = useState<HistoryEvent | null>(null); + const [detailMode, setDetailMode] = useState<DetailMode>("conversation"); + const [conversation, setConversation] = useState< + TaskConversationResponse | SupervisorConversationResponse | null + >(null); + const [checkpoints, setCheckpoints] = useState<TaskCheckpoint[]>([]); + const [detailLoading, setDetailLoading] = useState(false); + + // Redirect to login if not authenticated + useEffect(() => { + if (!authLoading && isAuthConfigured && !isAuthenticated) { + navigate("/login"); + } + }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); + + // Load contracts for filter dropdown + useEffect(() => { + async function loadContracts() { + try { + const response = await listContracts(); + setContracts(response.contracts); + } catch (e) { + console.error("Failed to load contracts:", e); + } + } + if (isAuthenticated || !isAuthConfigured) { + loadContracts(); + } + }, [isAuthenticated, isAuthConfigured]); + + // Load timeline + const loadTimeline = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await getTimeline({ + contractId: selectedContractId || undefined, + taskId: selectedTaskId || undefined, + from: dateFrom || undefined, + to: dateTo || undefined, + limit: 100, + }); + setEvents(response.entries); + setTotalCount(response.totalCount); + } catch (e) { + console.error("Failed to load timeline:", e); + setError(e instanceof Error ? e.message : "Failed to load timeline"); + } finally { + setLoading(false); + } + }, [selectedContractId, selectedTaskId, dateFrom, dateTo]); + + // Load timeline on mount and filter change + useEffect(() => { + if (isAuthenticated || !isAuthConfigured) { + loadTimeline(); + } + }, [loadTimeline, isAuthenticated, isAuthConfigured]); + + // Load detail when event selected + const handleSelectEvent = useCallback(async (event: HistoryEvent) => { + setSelectedEvent(event); + setDetailLoading(true); + + try { + // Determine if this is a task or supervisor event + if (event.taskId) { + // Load task conversation and checkpoints + const [conv, cps] = await Promise.all([ + getTaskConversation(event.taskId, { + includeToolCalls: true, + includeToolResults: true, + }), + getTaskCheckpoints(event.taskId).catch(() => []), + ]); + setConversation(conv); + setCheckpoints(cps); + } else if (event.contractId) { + // Load supervisor conversation + const conv = await getSupervisorConversation(event.contractId); + setConversation(conv); + setCheckpoints([]); + } + } catch (e) { + console.error("Failed to load event details:", e); + } finally { + setDetailLoading(false); + } + }, []); + + // Handle URL param for direct navigation + useEffect(() => { + if (id && events.length > 0) { + const event = events.find((e) => e.taskId === id || e.contractId === id); + if (event && event !== selectedEvent) { + handleSelectEvent(event); + } + } + }, [id, events, selectedEvent, handleSelectEvent]); + + // Clear selection + const handleClearSelection = useCallback(() => { + setSelectedEvent(null); + setConversation(null); + setCheckpoints([]); + navigate("/history"); + }, [navigate]); + + // Handle filter changes + const handleContractChange = useCallback((contractId: string | null) => { + setSelectedContractId(contractId); + setSelectedTaskId(null); // Reset task filter when contract changes + }, []); + + // Handle actions completed + const handleActionComplete = useCallback(() => { + // Refresh timeline and detail after action + loadTimeline(); + if (selectedEvent?.taskId) { + handleSelectEvent(selectedEvent); + } + }, [loadTimeline, selectedEvent, handleSelectEvent]); + + if (authLoading) { + return ( + <div className="relative z-10 min-h-screen bg-[#0a1628] flex flex-col"> + <Masthead /> + <div className="flex-1 flex items-center justify-center"> + <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> + </div> + </div> + ); + } + + return ( + <div className="relative z-10 min-h-screen bg-[#0a1628] flex flex-col"> + <Masthead /> + + <main className="flex-1 flex flex-col overflow-hidden p-4 gap-4"> + {/* Filters */} + <HistoryFilters + contracts={contracts} + selectedContractId={selectedContractId} + onContractChange={handleContractChange} + dateFrom={dateFrom} + dateTo={dateTo} + onDateFromChange={setDateFrom} + onDateToChange={setDateTo} + totalCount={totalCount} + /> + + {/* Main content area */} + <div className="flex-1 flex gap-4 min-h-0 overflow-hidden"> + {/* Timeline list */} + <div className="w-1/3 min-w-[300px] max-w-[400px] flex flex-col"> + <TimelineList + events={events} + loading={loading} + error={error} + selectedEvent={selectedEvent} + onSelectEvent={handleSelectEvent} + onRefresh={loadTimeline} + /> + </div> + + {/* Detail panel */} + <div className="flex-1 flex flex-col min-h-0 overflow-hidden panel"> + {selectedEvent ? ( + <> + {/* Detail header */} + <div className="shrink-0 p-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between"> + <div className="flex items-center gap-4"> + <button + onClick={handleClearSelection} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg + className="w-5 h-5" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M15 19l-7-7 7-7" + /> + </svg> + </button> + <div> + <div className="font-mono text-xs text-[#9bc3ff] uppercase"> + {selectedEvent.eventType} + {selectedEvent.eventSubtype && ` / ${selectedEvent.eventSubtype}`} + </div> + <div className="font-mono text-[10px] text-[#7788aa]"> + {new Date(selectedEvent.createdAt).toLocaleString()} + </div> + </div> + </div> + + {/* Mode toggle (only if task has checkpoints) */} + {checkpoints.length > 0 && ( + <div className="flex gap-1"> + <button + onClick={() => setDetailMode("conversation")} + className={`px-3 py-1 font-mono text-[10px] uppercase border transition-colors ${ + detailMode === "conversation" + ? "border-[#3f6fb3] text-[#9bc3ff] bg-[rgba(63,111,179,0.2)]" + : "border-[rgba(117,170,252,0.25)] text-[#7788aa] hover:border-[rgba(117,170,252,0.35)]" + }`} + > + Conversation + </button> + <button + onClick={() => setDetailMode("checkpoints")} + className={`px-3 py-1 font-mono text-[10px] uppercase border transition-colors ${ + detailMode === "checkpoints" + ? "border-[#3f6fb3] text-[#9bc3ff] bg-[rgba(63,111,179,0.2)]" + : "border-[rgba(117,170,252,0.25)] text-[#7788aa] hover:border-[rgba(117,170,252,0.35)]" + }`} + > + Checkpoints ({checkpoints.length}) + </button> + </div> + )} + </div> + + {/* Detail content */} + <div className="flex-1 overflow-auto"> + {detailLoading ? ( + <div className="flex items-center justify-center h-full"> + <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> + </div> + ) : detailMode === "conversation" && conversation ? ( + <ConversationView conversation={conversation} /> + ) : detailMode === "checkpoints" && checkpoints.length > 0 ? ( + <CheckpointList + checkpoints={checkpoints} + taskId={selectedEvent.taskId!} + onActionComplete={handleActionComplete} + /> + ) : ( + <div className="flex items-center justify-center h-full"> + <div className="font-mono text-[#7788aa] text-xs"> + No {detailMode} data available + </div> + </div> + )} + </div> + + {/* Resume controls */} + {selectedEvent.taskId && ( + <ResumeControls + taskId={selectedEvent.taskId} + contractId={selectedEvent.contractId} + checkpoints={checkpoints} + onActionComplete={handleActionComplete} + /> + )} + </> + ) : ( + <div className="flex items-center justify-center h-full"> + <div className="text-center"> + <div className="font-mono text-[#7788aa] text-sm mb-2"> + Select an event to view details + </div> + <div className="font-mono text-[#556677] text-xs"> + View conversation history, checkpoints, and more + </div> + </div> + </div> + )} + </div> + </div> + </main> + </div> + ); +} |
