"use client"; import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Flag, Clock, Hash, ArrowRight, Bot, User, ChevronDown, ChevronUp, Info, } from "lucide-react"; import { useInference } from "@/lib/inference"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { API_BASE } from "@/lib/constants"; import ForceDirectedGraph from "./force-directed-graph"; import qwen3Data from "../../results/qwen3.json" // Simple Switch component since it's not available in the UI components const Switch = ({ checked, onCheckedChange, disabled, id, }: { checked: boolean; onCheckedChange: (checked: boolean) => void; disabled?: boolean; id?: string; }) => { return ( ); }; type Message = { role: "user" | "assistant" | "game" | "result" | "error"; content: string; metadata?: { page?: string; links?: string[]; status?: "playing" | "won" | "lost"; path?: string[]; }; }; const buildPrompt = ( current: string, target: string, path_so_far: string[], links: string[] ) => { const formatted_links = links .map((link, index) => `${index + 1}. ${link}`) .join("\n"); const path_so_far_str = path_so_far.join(" -> "); return `You are playing WikiRun, trying to navigate from one Wikipedia article to another using only links. IMPORTANT: You MUST put your final answer in NUMBER tags, where NUMBER is the link number. For example, if you want to choose link 3, output 3. Current article: ${current} Target article: ${target} You have ${links.length} link(s) to choose from: ${formatted_links} Your path so far: ${path_so_far_str} Think about which link is most likely to lead you toward the target article. First, analyze each link briefly and how it connects to your goal, then select the most promising one. Remember to format your final answer by explicitly writing out the xml number tags like this: NUMBER`; }; interface GameComponentProps { player: "me" | "model"; model?: string; maxHops: number; startPage: string; targetPage: string; onReset: () => void; maxTokens: number; maxLinks: number; } export default function GameComponent({ player, model, maxHops, startPage, targetPage, onReset, maxTokens, maxLinks, }: GameComponentProps) { const [currentPage, setCurrentPage] = useState(startPage); const [currentPageLinks, setCurrentPageLinks] = useState([]); const [linksLoading, setLinksLoading] = useState(false); const [hops, setHops] = useState(0); const [timeElapsed, setTimeElapsed] = useState(0); const [visitedNodes, setVisitedNodes] = useState([startPage]); const [gameStatus, setGameStatus] = useState<"playing" | "won" | "lost">( "playing" ); const [continuousPlay, setContinuousPlay] = useState(true); const [autoRunning, setAutoRunning] = useState(true); const [convo, setConvo] = useState([]); const [expandedMessages, setExpandedMessages] = useState< Record >({ game: false }); const messagesEndRef = useRef(null); const { status: modelStatus, partialText, inference } = useInference({ apiKey: window.localStorage.getItem("huggingface_access_token") || undefined, }); const fetchCurrentPageLinks = useCallback(async () => { setLinksLoading(true); const response = await fetch( `${API_BASE}/get_article_with_links/${currentPage}` ); const data = await response.json(); setCurrentPageLinks(data.links.slice(0, maxLinks)); setLinksLoading(false); }, [currentPage, maxLinks]); useEffect(() => { fetchCurrentPageLinks(); }, [fetchCurrentPageLinks]); useEffect(() => { if (gameStatus === "playing") { const timer = setInterval(() => { setTimeElapsed((prev) => prev + 1); }, 1000); return () => clearInterval(timer); } }, [gameStatus]); // Check win condition useEffect(() => { if (currentPage === targetPage) { setGameStatus("won"); } else if (hops >= maxHops) { setGameStatus("lost"); } }, [currentPage, targetPage, hops, maxHops]); const handleLinkClick = (link: string) => { if (gameStatus !== "playing") return; setCurrentPage(link); setHops((prev) => prev + 1); setVisitedNodes((prev) => [...prev, link]); }; const currentRuns = useMemo(() => { const q3runs = qwen3Data.runs.filter((run) => run.result === "win"); if (visitedNodes.length === 0) { return q3runs; } return [ { steps: [ { type: "start", article: startPage, }, ...visitedNodes.map((node) => ({ type: "move", article: node })), ], start_article: startPage, destination_article: targetPage, }, ...q3runs, ]; }, [visitedNodes, startPage, targetPage]); const makeModelMove = async () => { const prompt = buildPrompt( currentPage, targetPage, visitedNodes, currentPageLinks ); pushConvo({ role: "user", content: prompt, }); const {status, result: modelResponse} = await inference({ model: model, prompt, maxTokens: maxTokens, }); if (status === "error") { pushConvo({ role: "error", content: "Error during inference: " + modelResponse, }); setAutoRunning(false); return; } pushConvo({ role: "assistant", content: modelResponse, }); console.log("Model response", modelResponse); const answer = modelResponse.match(/(.*?)<\/answer>/)?.[1]; if (!answer) { console.error("No answer found in model response"); return; } // try parsing the answer as an integer const answerInt = parseInt(answer); if (isNaN(answerInt)) { console.error("Invalid answer found in model response"); return; } if (answerInt < 1 || answerInt > currentPageLinks.length) { console.error( "Selected link out of bounds", answerInt, "from ", currentPageLinks.length, "links" ); return; } const selectedLink = currentPageLinks[answerInt - 1]; // Add a game status message after each move pushConvo({ role: "game", content: `Model selected link ${answerInt}: ${selectedLink}`, metadata: { page: currentPage, links: [...currentPageLinks], }, }); console.log( "Model picked selectedLink", selectedLink, "from ", currentPageLinks ); handleLinkClick(selectedLink); }; const handleGiveUp = () => { setGameStatus("lost"); }; const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs < 10 ? "0" : ""}${secs}`; }; const pushConvo = (message: Message) => { setConvo((prev) => [...prev, message]); }; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [convo, partialText]); const toggleMessageExpand = (index: number | string) => { setExpandedMessages((prev) => ({ ...prev, [index]: !prev[index], })); }; // Effect for continuous play mode useEffect(() => { if ( continuousPlay && autoRunning && player === "model" && gameStatus === "playing" && modelStatus !== "thinking" && !linksLoading ) { const timer = setTimeout(() => { makeModelMove(); }, 1000); return () => clearTimeout(timer); } }, [ continuousPlay, autoRunning, player, gameStatus, modelStatus, linksLoading, currentPage, ]); // Add a result message when the game ends useEffect(() => { if (gameStatus !== "playing" && convo.length > 0 && convo[convo.length - 1].role !== "result") { pushConvo({ role: "result", content: gameStatus === "won" ? `${model} successfully navigated from ${visitedNodes[0]} to ${targetPage} in ${hops} moves!` : `${model} failed to reach ${targetPage} within the ${maxHops} hop limit.`, metadata: { status: gameStatus, path: [...visitedNodes], }, }); } }, [gameStatus]); return (
{/* Condensed Game Status Card */}
{currentPage}
{targetPage}
{hops} / {maxHops}
Path: {visitedNodes.join(" → ")}
{formatTime(timeElapsed)}
{gameStatus === "playing" && ( <> {player === "model" && ( <> {continuousPlay ? ( ) : ( )}
{ setContinuousPlay(checked); if (!checked) setAutoRunning(false); }} disabled={ modelStatus === "thinking" || linksLoading || (continuousPlay && autoRunning) } />
)} {player === "me" && ( )} )} {gameStatus !== "playing" && ( )}
{/* Links panel - larger now */} {player === "me" && (

Available Links Why are some links missing?

We're playing on a pruned version of Simple Wikipedia so that every path between articles is possible. See dataset details{" "} here .

{gameStatus === "playing" ? (
{currentPageLinks .sort((a, b) => a.localeCompare(b)) .map((link) => ( ))}
) : (
{gameStatus === "won" ? (

You won!

You reached {targetPage} in {hops} {hops === 1 ? 'hop' : 'hops'} in {formatTime(timeElapsed)}

Your Path:

{visitedNodes.map((node, index) => (
{node} {index < visitedNodes.length - 1 && ( )}
))}
) : (

Game Over

You didn't reach {targetPage} within {maxHops} hops.

)}
)}
)} {/* Reasoning panel - spans full height on left side */} {player === "model" && (

LLM Reasoning

{convo.map((message, index) => { const isExpanded = expandedMessages[index] || false; if (message.role === "user" || message.role === "assistant") { const isLongUserMessage = message.role === "user" && message.content.length > 300; const shouldTruncate = isLongUserMessage && !isExpanded; return (
{message.role === "assistant" ? ( <> Assistant ) : ( <> User )}

{shouldTruncate ? message.content.substring(0, 300) + "..." : message.content}

{isLongUserMessage && ( )}
); } else if (message.role === "game") { // Game status block return (
Game Status

{message.content}

{message.metadata?.page && (

Current page: {message.metadata.page}

)} {message.metadata?.links && (

Available links: {message.metadata.links.length} {!isExpanded && message.metadata.links.length > 0 && ( {" "}({message.metadata.links.slice(0, 3).join(", ")} {message.metadata.links.length > 3 ? "..." : ""}) )}

)} {isExpanded && message.metadata?.links && (
{message.metadata.links.map((link, i) => (
{i+1}. {link}
))}
)}
); } else if (message.role === "result") { // Result block const isWon = message.metadata?.status === "won"; return (
{isWon ? (

{message.content}

{message.metadata?.path && (

Path:

{message.metadata.path.map((node, index) => (
{node} {index < message.metadata.path.length - 1 && ( )}
))}
)}
) : (
Game Over

{message.content}

{message.metadata?.path && (

Path: {message.metadata.path.join(" → ")}

)}
)}
); } else if (message.role === "error") { return (

{message.content}

); } return null; })} {modelStatus === "thinking" && (
Thinking...

{partialText}

)}
)} {/* Wikipedia view - top right quadrant */}

Wikipedia View