import { useState, useRef, useEffect } from "react"; import { useRouterState, Link } from "@tanstack/react-router"; import { useMutation } from "@tanstack/react-query"; import { MaterialIcon } from "@/components/ui/MaterialIcon"; import { useGenerationJobs, type ActiveJob } from "@/hooks/use-generation-jobs"; import { isTerminal } from "@/hooks/use-job-shared"; import { trpc, queryClient } from "@/utils/trpc"; interface LogEntry { step: string; message: string; status: "running" | "done" | "error"; timestamp: string; details?: string; } const STEP_LABELS: Record = { generate_passage: "Menulis Bacaan", validate_passage: "Memvalidasi Bacaan", generate_questions: "Membuat Soal", self_validate: "Self-Check Kualitas", save: "Menyimpan Hasil", cancel: "Dibatalkan", error: "Error", }; function LogStatusIcon({ status }: { status: string }) { if (status === "done") return ( ); if (status === "error") return ( ); return ( ); } function TerminalLog({ logs, isRunning }: { logs: LogEntry[]; isRunning: boolean }) { const scrollRef = useRef(null); const [expandedIdx, setExpandedIdx] = useState(null); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [logs]); if (!logs?.length) { return (
AI sedang mempersiapkan...
); } return (
{logs.map((log, i) => { const isExpanded = expandedIdx === i; const hasDetails = !!log.details && log.details.length > 0; const isLast = i === logs.length - 1; const isCurrentRunning = isLast && log.status === "running" && isRunning; return (
hasDetails && setExpandedIdx(isExpanded ? null : i)} > {new Date(log.timestamp).toLocaleTimeString("id-ID", { hour: "2-digit", minute: "2-digit", })} {STEP_LABELS[log.step] ?? log.step} {isCurrentRunning && ( )}
{isExpanded && hasDetails && (
{log.details}
)}
); })}
); } function SingleJobCard({ job, onCancel, cancelPending, }: { job: ActiveJob; onCancel: (jobId: string) => void; cancelPending: boolean; }) { const [minimized, setMinimized] = useState(false); const progress = job.progress ?? 0; const message = job.progressMessage ?? "Sedang berjalan..."; const logs = (job.logs ?? []) as LogEntry[]; const isAgentic = job.mode === "agentic"; if (minimized) { return ( ); } return (
{message} {isAgentic && ( Agentic )}
{progress}%
{isAgentic && }
); } export function GlobalGenerationProgress() { const { activeJobs, isGenerating, removeJob, setError } = useGenerationJobs(); const [collapsed, setCollapsed] = useState(false); const matches = useRouterState({ select: (s) => s.matches }); const isGeneratePage = matches.some((m) => m.routeId === "/generate"); const isTakePage = matches.some((m) => m.routeId === "/package/$id/take"); const cancelMutation = useMutation({ ...trpc.ai.cancelJob.mutationOptions(), onSuccess: async (_data, variables) => { removeJob(variables.jobId); setError(null); await queryClient.invalidateQueries({ queryKey: trpc.ai.myJobs.queryKey(), }); }, }); if (!isGenerating || isTakePage) return null; const handleCancel = (jobId: string) => { cancelMutation.mutate({ jobId }); }; // Collapsed floating pill showing count if (collapsed) { return ( ); } return (
AI Generation{activeJobs.length > 1 ? ` (${activeJobs.length})` : ""}
{activeJobs.map((job) => ( ))}
); }