stillerman's picture
stillerman HF Staff
Small screen warning, qwen default
7a7f75f
"use client";
import { useEffect, useState } from "react";
import { Card } from "@/components/ui/card";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import GameComponent from "@/components/game-component";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { API_BASE } from "@/lib/constants";
import { VirtualizedCombobox } from "./ui/virtualized-combobox";
import { Info, Shuffle } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Separator } from "@/components/ui/separator";
import popularNodes from "../../results/popular_nodes.json";
export default function PlayTab({
startArticle,
destinationArticle,
}: {
startArticle?: string;
destinationArticle?: string;
}) {
const [player, setPlayer] = useState<"me" | "model">("model");
const [selectedModel, setSelectedModel] = useState<string | undefined>(
"Qwen/Qwen3-30B-A3B"
);
const [maxHops, setMaxHops] = useState<number>(20);
const [isGameStarted, setIsGameStarted] = useState<boolean>(false);
const [startPage, setStartPage] = useState<string>(
startArticle || "Capybara"
);
const [targetPage, setTargetPage] = useState<string>(
destinationArticle || "Pokémon"
);
const [maxTokens, setMaxTokens] = useState<number>(3000);
const [maxLinks, setMaxLinks] = useState<number>(200);
const [isServerConnected, setIsServerConnected] = useState<boolean>(false);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [modelList, setModelList] = useState<string[]>([
"deepseek-ai/DeepSeek-V3-0324",
"Qwen/Qwen3-235B-A22B",
"Qwen/Qwen3-30B-A3B",
"Qwen/Qwen3-14B",
"google/gemma-3-27b-it",
]);
const [allArticles, setAllArticles] = useState<string[]>([]);
// Server connection check
useEffect(() => {
const checkServerConnection = async () => {
try {
const response = await fetch(API_BASE + "/health");
setIsServerConnected(response.ok);
} catch {
setIsServerConnected(false);
}
};
// Check immediately and then every 30 seconds
checkServerConnection();
const interval = setInterval(checkServerConnection, 30000);
return () => clearInterval(interval);
}, []);
// Authentication check
useEffect(() => {
const checkAuthentication = () => {
const idToken = window.localStorage.getItem("huggingface_id_token");
const accessToken = window.localStorage.getItem(
"huggingface_access_token"
);
if (idToken && accessToken) {
try {
const idTokenObject = JSON.parse(idToken);
if (idTokenObject.exp > Date.now() / 1000) {
setIsAuthenticated(true);
return;
}
} catch (error) {
console.error("Error parsing ID token:", error);
}
}
setIsAuthenticated(false);
};
checkAuthentication();
window.addEventListener("storage", checkAuthentication);
return () => {
window.removeEventListener("storage", checkAuthentication);
};
}, []);
useEffect(() => {
const fetchAllArticles = async () => {
const response = await fetch(`${API_BASE}/get_all_articles`);
const data = await response.json();
setAllArticles(data);
};
fetchAllArticles();
}, []);
const handleStartGame = () => {
setIsGameStarted(true);
};
const handleResetGame = () => {
setIsGameStarted(false);
};
const handlePlayerChange = (value: string) => {
setPlayer(value as "me" | "model");
};
const selectRandomArticle = (setter: (article: string) => void) => {
if (popularNodes.length > 0) {
const randomIndex = Math.floor(Math.random() * popularNodes.length);
setter(popularNodes[randomIndex]);
}
};
return (
<div className="space-y-6">
{!isGameStarted ? (
<div className="space-y-6">
<Card className="p-6">
<h3 className="text-lg font-medium mb-4">Game Setup</h3>
<div className="space-y-6">
<div>
<h4 className="text-sm font-medium mb-3">Player Mode</h4>
<Tabs
defaultValue="me"
value={player}
onValueChange={handlePlayerChange}
className="w-full"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="me">I'll Play</TabsTrigger>
<TabsTrigger value="model">AI Will Play</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Separator className="my-4" />
<div className="grid grid-cols-1 md:grid-cols-1 gap-6">
<div>
<h4 className="text-sm font-medium mb-3">Start Page</h4>
<div className="flex items-center">
<VirtualizedCombobox
options={allArticles}
value={startPage}
onValueChange={(value) => setStartPage(value)}
/>
<Button
variant="outline"
size="sm"
onClick={() => selectRandomArticle(setStartPage)}
className="h-9 ml-2 whitespace-nowrap"
>
<Shuffle className="h-3.5 w-3.5 mr-1" />
Random
</Button>
{/* absorb rest of width */}
<div className="flex-1" />
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-3">Target Page</h4>
<div className="flex items-center">
<VirtualizedCombobox
options={allArticles}
value={targetPage}
onValueChange={(value) => setTargetPage(value)}
/>
<Button
variant="outline"
size="sm"
onClick={() => selectRandomArticle(setTargetPage)}
className="h-9 ml-2 whitespace-nowrap"
>
<Shuffle className="h-3.5 w-3.5 mr-1" />
Random
</Button>
{/* absorb rest of width */}
<div className="flex-1" />
</div>
</div>
</div>
{player === "model" && (
<>
<Separator className="my-4" />
<div className="animate-in fade-in slide-in-from-top-5 duration-300">
<h4 className="text-sm font-medium mb-3">Model Settings</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label
htmlFor="model-select"
className="flex items-center gap-1 text-sm mb-2"
>
Select Model
</Label>
<Select
value={selectedModel}
onValueChange={setSelectedModel}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={`Select a model (${modelList.length} available)`}
/>
</SelectTrigger>
<SelectContent>
{modelList.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label
htmlFor="max-tokens"
className="flex items-center gap-1 text-sm mb-2"
>
Max Tokens
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info className="h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
Maximum number of tokens the model can
generate per response.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
id="max-tokens"
type="number"
value={maxTokens}
onChange={(e) =>
setMaxTokens(Number.parseInt(e.target.value))
}
min={1}
max={10000}
/>
</div>
<div>
<Label
htmlFor="max-links"
className="flex items-center gap-1 text-sm mb-2"
>
Max Links
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info className="h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
Maximum number of links the model can consider
per page. Small models tend to get stuck if
this is too high.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
id="max-links"
type="number"
value={maxLinks}
onChange={(e) =>
setMaxLinks(Number.parseInt(e.target.value))
}
min={1}
max={1000}
/>
</div>
</div>
</div>
</>
)}
</div>
</Card>
<div className="flex justify-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<Button
onClick={handleStartGame}
size="lg"
className="px-8"
variant="default"
disabled={!isAuthenticated && player === "model"}
>
Start Game
</Button>
</div>
</TooltipTrigger>
{!isAuthenticated && player === "model" && (
<TooltipContent>
<p className="max-w-xs">
Please sign in with Hugging Face to play the game
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
{!isServerConnected && (
<div className="text-center p-2 bg-yellow-100 text-yellow-800 rounded-md text-sm">
Server connection issue. Some features may be unavailable.
</div>
)}
</div>
) : (
<GameComponent
player={player}
model={player === "model" ? selectedModel : undefined}
maxHops={maxHops}
startPage={startPage}
targetPage={targetPage}
onReset={handleResetGame}
maxTokens={maxTokens}
maxLinks={maxLinks}
/>
)}
</div>
);
}