| import { useEffect, useState } from "react"; | |
| import type { GepaIterData, PanelNav } from "../types"; | |
| interface GepaIterLevelProps { | |
| datasetId: string; | |
| gepaIter: number; | |
| fetchGepaIter: (dsId: string, gepaIter: number) => Promise<GepaIterData>; | |
| onDrillDown: (nav: PanelNav) => void; | |
| } | |
| export default function GepaIterLevel({ | |
| datasetId, | |
| gepaIter, | |
| fetchGepaIter, | |
| onDrillDown, | |
| }: GepaIterLevelProps) { | |
| const [data, setData] = useState<GepaIterData | null>(null); | |
| useEffect(() => { | |
| fetchGepaIter(datasetId, gepaIter).then(setData).catch(() => {}); | |
| }, [datasetId, gepaIter, fetchGepaIter]); | |
| if (!data) return <div className="p-4 text-gray-400">Loading GEPA iteration...</div>; | |
| return ( | |
| <div className="p-4 space-y-4"> | |
| {/* Stats row */} | |
| <div className="flex gap-4 text-xs text-gray-400"> | |
| <span> | |
| Total tokens:{" "} | |
| <span className="text-gray-200"> | |
| {((data.total_input_tokens + data.total_output_tokens) / 1000).toFixed(1)}k | |
| </span> | |
| </span> | |
| <span> | |
| Time: <span className="text-gray-200">{data.total_execution_time.toFixed(1)}s</span> | |
| </span> | |
| <span> | |
| RLM Calls: <span className="text-gray-200">{data.rlm_calls.length}</span> | |
| </span> | |
| </div> | |
| {/* RLM iteration timeline */} | |
| {data.rlm_calls.map((call) => ( | |
| <div key={call.rlm_call_idx}> | |
| {data.rlm_calls.length > 1 && ( | |
| <div className="text-xs font-semibold text-gray-400 mb-2"> | |
| RLM Call {call.rlm_call_idx} | |
| </div> | |
| )} | |
| <div className="flex gap-2 overflow-x-auto pb-2"> | |
| {call.iterations.map((it) => ( | |
| <div | |
| key={it.rlm_iter} | |
| className={`flex-shrink-0 w-56 bg-gray-800 border rounded-lg p-3 cursor-pointer transition-colors hover:border-orange-500 ${ | |
| it.has_final_answer ? "border-emerald-600" : "border-gray-700" | |
| }`} | |
| onClick={() => | |
| onDrillDown({ | |
| datasetId, | |
| level: 3, | |
| gepaIter, | |
| rlmCallIdx: call.rlm_call_idx, | |
| rlmIter: it.rlm_iter, | |
| }) | |
| } | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="bg-gray-700 text-gray-200 text-xs font-mono px-2 py-0.5 rounded"> | |
| iter {it.rlm_iter} | |
| </span> | |
| <div className="flex gap-1"> | |
| {it.has_code_blocks && ( | |
| <span className="bg-emerald-900 text-emerald-300 text-xs px-1.5 py-0.5 rounded"> | |
| {it.n_code_blocks} code | |
| </span> | |
| )} | |
| {it.has_final_answer && ( | |
| <span className="bg-amber-900 text-amber-300 text-xs px-1.5 py-0.5 rounded"> | |
| FINAL | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex justify-between text-xs text-gray-500 mb-2"> | |
| <span>{((it.input_tokens + it.output_tokens) / 1000).toFixed(1)}k tok</span> | |
| <span>{it.execution_time.toFixed(1)}s</span> | |
| </div> | |
| <div className="text-xs text-gray-400 line-clamp-3 leading-relaxed"> | |
| {it.response_preview || "(empty)"} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| {/* Final answer if present */} | |
| {data.final_answer && ( | |
| <div className="bg-emerald-950 border border-emerald-700 rounded-lg p-4"> | |
| <div className="text-xs font-semibold text-emerald-400 mb-2">Final Answer</div> | |
| <div className="text-sm text-gray-200 whitespace-pre-wrap max-h-60 overflow-y-auto"> | |
| {data.final_answer} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |