"use client"; import { useState, useEffect, useCallback, useRef } 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"; // 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"; content: 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(false); const [autoRunning, setAutoRunning] = useState(true); const [convo, setConvo] = useState([]); const [expandedMessages, setExpandedMessages] = useState>({}); 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 makeModelMove = async () => { const prompt = buildPrompt( currentPage, targetPage, visitedNodes, currentPageLinks ); pushConvo({ role: "user", content: prompt, }); const modelResponse = await inference({ model: model, prompt, maxTokens: maxTokens, }); 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]; 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) => { 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]); 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 */}

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" ? (

{player === "model" ? `${model} won!` : "You won!"}

{player === "model" ? "It" : "You"} reached {targetPage} in{" "} {hops} hops.

) : (

Game Over

{player === "model" ? `${model} didn't` : "You didn't"} reach{" "} {targetPage} within {maxHops} hops.

)}
)}
{/* Reasoning panel - larger now */} {player === "model" && (

LLM Reasoning

{convo.map((message, index) => { const isExpanded = expandedMessages[index] || false; 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 && ( )}
); })} {modelStatus === "thinking" && (
Thinking...

{partialText}

)}
)} {player === "me" && (

Wikipedia View