export default function ViewerTab()

in src/components/viewer-tab.tsx [47:265]


export default function ViewerTab({
  handleTryRun,
}: {
  handleTryRun: (startArticle: string, destinationArticle: string) => void;
}) {
  const [selectedRun, setSelectedRun] = useState<number | null>(null);
  const [runs, setRuns] = useState<Run[]>([]);
  const [selectedModel, setSelectedModel] = useState<string>("Qwen3-14B");
  const [modelStats, setModelStats] = useState<ModelStats | null>(null);
  const [models, setModels] = useState(defaultModels);
  const fileInputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // Convert the model data to the format expected by RunsList
    const convertedRuns = models[selectedModel]?.runs?.map((run: {
      start_article: string;
      destination_article: string;
      steps: { type: string; article: string }[];
      result: string;
    }) => ({
      start_article: run.start_article,
      destination_article: run.destination_article,
      steps: run.steps.map((step: { article: string }) => step.article),
      result: run.result
    })) || [];
    setRuns(convertedRuns);

    // Calculate model statistics
    const winRuns = convertedRuns.filter(run => run.result === "win");
    const totalRuns = convertedRuns.length;
    const wins = winRuns.length;
    const winPercentage = totalRuns > 0 ? (wins / totalRuns) * 100 : 0;
    
    // Calculate steps statistics for winning runs
    const stepCounts = winRuns.map(run => run.steps.length);
    const avgSteps = stepCounts.length > 0 
      ? stepCounts.reduce((sum, count) => sum + count, 0) / stepCounts.length 
      : 0;
    
    // Calculate standard deviation
    const variance = stepCounts.length > 0
      ? stepCounts.reduce((sum, count) => sum + Math.pow(count - avgSteps, 2), 0) / stepCounts.length
      : 0;
    const stdDevSteps = Math.sqrt(variance);

    // Calculate median, min, max steps
    const sortedSteps = [...stepCounts].sort((a, b) => a - b);
    const medianSteps = stepCounts.length > 0
      ? stepCounts.length % 2 === 0
        ? (sortedSteps[stepCounts.length / 2 - 1] + sortedSteps[stepCounts.length / 2]) / 2
        : sortedSteps[Math.floor(stepCounts.length / 2)]
      : 0;
    const minSteps = stepCounts.length > 0 ? Math.min(...stepCounts) : 0;
    const maxSteps = stepCounts.length > 0 ? Math.max(...stepCounts) : 0;

    setModelStats({
      winPercentage,
      avgSteps,
      stdDevSteps,
      totalRuns,
      wins,
      medianSteps,
      minSteps,
      maxSteps
    });
  }, [selectedModel, models]);

  const handleRunSelect = (runId: number) => {
    setSelectedRun(runId);
  };

  const filterRuns = useMemo(() => {
    return runs.filter(run => run.result === "win");
  }, [runs]);

  // Convert the runs to the format expected by ForceDirectedGraph
  const forceGraphRuns = useMemo(() => {
    return filterRuns.map((run): ForceGraphRun => ({
      start_article: run.start_article,
      destination_article: run.destination_article,
      steps: run.steps.map(article => ({ type: "move", article }))
    }));
  }, [filterRuns]);

  const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const jsonData = JSON.parse(e.target?.result as string);
        
        // Validate the JSON structure has the required fields
        if (!jsonData.runs || !Array.isArray(jsonData.runs)) {
          alert("Invalid JSON format. File must contain a 'runs' array.");
          return;
        }
        
        // Create a filename-based model name, removing extension and path
        const fileName = file.name.replace(/\.[^/.]+$/, "");
        const modelName = `Custom: ${fileName}`;
        
        // Add the new model to the models object
        setModels(prev => ({
          ...prev,
          [modelName]: jsonData
        }));
        
        // Select the newly added model
        setSelectedModel(modelName);
      } catch (error) {
        alert(`Error parsing JSON file: ${error.message}`);
      }
    };
    reader.readAsText(file);
    
    // Reset the file input
    if (fileInputRef.current) {
      fileInputRef.current.value = '';
    }
  };

  const handleUploadClick = () => {
    fileInputRef.current?.click();
  };

  return (
    <div className="grid grid-cols-1 md:grid-cols-12 gap-4 h-[calc(100vh-200px)] max-h-[calc(100vh-200px)] overflow-hidden p-2">
     <Card className="p-3 col-span-12 row-start-1">
       <div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
         <div className="flex-shrink-0">
           <Select value={selectedModel} onValueChange={setSelectedModel}>
             <SelectTrigger className="w-[180px]">
               <SelectValue placeholder="Select model" />
             </SelectTrigger>
             <SelectContent>
               {Object.keys(models).map((modelName) => (
                 <SelectItem key={modelName} value={modelName}>
                   {modelName}
                 </SelectItem>
               ))}
             </SelectContent>
           </Select>
         </div>
         
         <Button 
           variant="outline" 
           size="sm" 
           className="flex items-center gap-1" 
           onClick={handleUploadClick}
         >
           <UploadIcon size={14} />
           <span>Upload JSON</span>
           <input 
             type="file" 
             ref={fileInputRef}
             accept=".json" 
             className="hidden" 
             onChange={handleFileUpload} 
           />
         </Button>
         
         {modelStats && (
           <div className="flex flex-wrap gap-1.5 items-center">
             <Badge variant="outline" className="px-2 py-0.5 flex gap-1 items-center">
               <span className="text-xs font-medium">Success:</span>
               <span className="text-xs font-semibold">{modelStats.winPercentage.toFixed(1)}%</span>
               <span className="text-xs text-muted-foreground">({modelStats.wins}/{modelStats.totalRuns})</span>
             </Badge>
             
             <Badge variant="outline" className="px-2 py-0.5 flex gap-1 items-center">
               <span className="text-xs font-medium">Mean:</span>
               <span className="text-xs font-semibold">{modelStats.avgSteps.toFixed(1)}</span>
               <span className="text-xs text-muted-foreground">±{modelStats.stdDevSteps.toFixed(1)}</span>
             </Badge>
             
             <Badge variant="outline" className="px-2 py-0.5 flex gap-1 items-center">
               <span className="text-xs font-medium">Median:</span>
               <span className="text-xs font-semibold">{modelStats.medianSteps.toFixed(1)}</span>
             </Badge>
             
             <Badge variant="outline" className="px-2 py-0.5 flex gap-1 items-center">
               <span className="text-xs font-medium">Min:</span>
               <span className="text-xs font-semibold">{modelStats.minSteps}</span>
             </Badge>
             
             <Badge variant="outline" className="px-2 py-0.5 flex gap-1 items-center">
               <span className="text-xs font-medium">Max:</span>
               <span className="text-xs font-semibold">{modelStats.maxSteps}</span>
             </Badge>
           </div>
         )}
       </div>
     </Card>
      <div className="md:col-span-3 flex flex-col max-h-full overflow-hidden">
        <div className="bg-card rounded-lg p-3 border flex-grow overflow-hidden flex flex-col">
          <h3 className="text-sm font-medium mb-2 text-muted-foreground flex-shrink-0">
            Runs
          </h3>
          <div className="flex-grow overflow-hidden">
            <RunsList
              runs={filterRuns}
              onSelectRun={handleRunSelect}
              selectedRunId={selectedRun}
              onTryRun={handleTryRun}
            />
          </div>
        </div>
      </div>

      <div className="md:col-span-9 max-h-full overflow-hidden">
        <Card className="w-full h-full flex items-center justify-center p-0 m-0 overflow-hidden">
          <ForceDirectedGraph runs={forceGraphRuns} runId={selectedRun} />
        </Card>
      </div>
    </div>
  );
}