summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/history.tsx
blob: fc88f0e9c9419a420869f265fcd9efb57223a972 (plain) (tree)




































































































































































































































































































































                                                                                                                         
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>
  );
}