stillerman commited on
Commit
27bba9c
·
1 Parent(s): d322f96

playable I think

Browse files
Dockerfile CHANGED
@@ -42,6 +42,8 @@ RUN curl -L https://huggingface.co/HuggingFaceTB/simplewiki-pruned-text-350k/res
42
 
43
  ENV WIKISPEEDIA_DB_PATH=/home/user/app/wikihop.db
44
 
 
 
45
  CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
46
 
47
 
 
42
 
43
  ENV WIKISPEEDIA_DB_PATH=/home/user/app/wikihop.db
44
 
45
+ ENV VITE_API_BASE=""
46
+
47
  CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "7860"]
48
 
49
 
api.py CHANGED
@@ -56,6 +56,10 @@ class SQLiteDB:
56
  links = json.loads(article["links_json"])
57
  return article["title"], links
58
 
 
 
 
 
59
  # Initialize database connection
60
  db = SQLiteDB(
61
  os.getenv("WIKISPEEDIA_DB_PATH", "/Users/jts/daily/wikihop/db/data/wikihop.db")
@@ -69,6 +73,11 @@ async def health_check():
69
  article_count=db._article_count
70
  )
71
 
 
 
 
 
 
72
  @app.get("/get_article_with_links/{article_title}", response_model=ArticleResponse)
73
  async def get_article(article_title: str):
74
  """Get article and its links by title"""
 
56
  links = json.loads(article["links_json"])
57
  return article["title"], links
58
 
59
+ def get_all_articles(self):
60
+ self.cursor.execute("SELECT title FROM core_articles")
61
+ return [row[0] for row in self.cursor.fetchall()]
62
+
63
  # Initialize database connection
64
  db = SQLiteDB(
65
  os.getenv("WIKISPEEDIA_DB_PATH", "/Users/jts/daily/wikihop/db/data/wikihop.db")
 
73
  article_count=db._article_count
74
  )
75
 
76
+ @app.get("/get_all_articles", response_model=List[str])
77
+ async def get_all_articles():
78
+ """Get all articles"""
79
+ return db.get_all_articles()
80
+
81
  @app.get("/get_article_with_links/{article_title}", response_model=ArticleResponse)
82
  async def get_article(article_title: str):
83
  """Get article and its links by title"""
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -13,13 +13,17 @@
13
  "dependencies": {
14
  "@huggingface/hub": "^1.1.2",
15
  "@huggingface/inference": "^3.10.0",
 
16
  "@radix-ui/react-label": "^2.1.4",
 
17
  "@radix-ui/react-select": "^2.2.2",
18
  "@radix-ui/react-slot": "^1.2.0",
19
  "@radix-ui/react-tabs": "^1.1.9",
20
  "@tailwindcss/vite": "^4.1.5",
 
21
  "class-variance-authority": "^0.7.1",
22
  "clsx": "^2.1.1",
 
23
  "d3": "^7.9.0",
24
  "jwt-decode": "^4.0.0",
25
  "lucide-react": "^0.503.0",
 
13
  "dependencies": {
14
  "@huggingface/hub": "^1.1.2",
15
  "@huggingface/inference": "^3.10.0",
16
+ "@radix-ui/react-dialog": "^1.1.11",
17
  "@radix-ui/react-label": "^2.1.4",
18
+ "@radix-ui/react-popover": "^1.1.11",
19
  "@radix-ui/react-select": "^2.2.2",
20
  "@radix-ui/react-slot": "^1.2.0",
21
  "@radix-ui/react-tabs": "^1.1.9",
22
  "@tailwindcss/vite": "^4.1.5",
23
+ "@tanstack/react-virtual": "^3.13.6",
24
  "class-variance-authority": "^0.7.1",
25
  "clsx": "^2.1.1",
26
+ "cmdk": "^1.1.1",
27
  "d3": "^7.9.0",
28
  "jwt-decode": "^4.0.0",
29
  "lucide-react": "^0.503.0",
src/components/game-component.tsx CHANGED
@@ -7,35 +7,8 @@ import { Flag, Clock, Hash, BarChart, ArrowRight, Bot } from "lucide-react";
7
  import inference from "@/lib/inference";
8
  import ReasoningTrace, { Run, Step } from "./reasoning-trace";
9
  import ForceDirectedGraph from "./force-directed-graph";
 
10
 
11
- const mockRun: Run = {
12
- steps: [
13
- {
14
- type: "start",
15
- article: "Dogs",
16
- metadata: {
17
- message: "Starting Node",
18
- },
19
- },
20
- {
21
- type: "step",
22
- article: "Dogs",
23
- links: ["Dogs", "Cats", "Birds"],
24
- metadata: {
25
- conversation: [
26
- {
27
- role: "user",
28
- content: "I want to go to the moon",
29
- },
30
- {
31
- role: "assistant",
32
- content: "I want to go to the moon",
33
- },
34
- ],
35
- },
36
- },
37
- ],
38
- };
39
  const buildPrompt = (
40
  current: string,
41
  target: string,
@@ -65,8 +38,6 @@ First, analyze each link briefly and how it connects to your goal, then select t
65
  Remember to format your final answer by explicitly writing out the xml number tags like this: <answer>NUMBER</answer>`;
66
  };
67
 
68
- const API_BASE = "";
69
-
70
  interface GameComponentProps {
71
  player: "me" | "model";
72
  model?: string;
@@ -97,6 +68,7 @@ export default function GameComponent({
97
  const [gameStatus, setGameStatus] = useState<"playing" | "won" | "lost">(
98
  "playing"
99
  );
 
100
  const [reasoningTrace, setReasoningTrace] = useState<Run | null>({
101
  start_article: startPage,
102
  destination_article: targetPage,
@@ -151,8 +123,17 @@ export default function GameComponent({
151
 
152
  const addStepToReasoningTrace = (step: Step) => {
153
  setReasoningTrace((prev) => {
154
- if (!prev) return { steps: [step], start_article: startPage, destination_article: targetPage };
155
- return { steps: [...prev.steps, step], start_article: startPage, destination_article: targetPage };
 
 
 
 
 
 
 
 
 
156
  });
157
  };
158
 
@@ -316,23 +297,6 @@ export default function GameComponent({
316
  )}
317
  </div>
318
 
319
- {/* Wikipedia iframe (mocked) */}
320
- {/* <div className="bg-muted/30 rounded-md flex-1 mb-4 overflow-hidden">
321
- <div className="bg-white p-4 border-b">
322
- <h2 className="text-xl font-bold">{currentPage}</h2>
323
- <p className="text-sm text-muted-foreground">
324
- https://en.wikipedia.org/wiki/{currentPage.replace(/\s+/g, "_")}
325
- </p>
326
- </div>
327
- <div className="p-4">
328
- <p className="text-sm">
329
- This is a mock Wikipedia page for {currentPage}. In the actual
330
- implementation, this would be an iframe showing the real Wikipedia
331
- page.
332
- </p>
333
- </div>
334
- </div> */}
335
-
336
  {/* Available links */}
337
  {gameStatus === "playing" && (
338
  <>
@@ -403,22 +367,33 @@ export default function GameComponent({
403
  </div>
404
  )}
405
  </Card>
406
-
407
  <Card className="p-4 flex flex-col max-h-[500px] overflow-y-auto">
408
  <ReasoningTrace run={reasoningTrace} />
 
 
 
 
 
 
 
 
 
 
409
  </Card>
410
 
411
  {/* Right pane - Game stats and graph */}
412
- <Card className="p-4 flex flex-col">
413
  <div className="flex-1 bg-muted/30 rounded-md p-4">
414
  <div className="flex items-center gap-2 text-sm font-medium text-muted-foreground mb-2">
415
  <BarChart className="h-4 w-4" /> Path Visualization
416
  </div>
417
 
418
- <ForceDirectedGraph runs={runs} runId={null} />
 
 
419
  </div>
420
- </Card>
421
  </div>
422
  );
423
  }
424
-
 
7
  import inference from "@/lib/inference";
8
  import ReasoningTrace, { Run, Step } from "./reasoning-trace";
9
  import ForceDirectedGraph from "./force-directed-graph";
10
+ import { API_BASE } from "@/lib/constants";
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  const buildPrompt = (
13
  current: string,
14
  target: string,
 
38
  Remember to format your final answer by explicitly writing out the xml number tags like this: <answer>NUMBER</answer>`;
39
  };
40
 
 
 
41
  interface GameComponentProps {
42
  player: "me" | "model";
43
  model?: string;
 
68
  const [gameStatus, setGameStatus] = useState<"playing" | "won" | "lost">(
69
  "playing"
70
  );
71
+
72
  const [reasoningTrace, setReasoningTrace] = useState<Run | null>({
73
  start_article: startPage,
74
  destination_article: targetPage,
 
123
 
124
  const addStepToReasoningTrace = (step: Step) => {
125
  setReasoningTrace((prev) => {
126
+ if (!prev)
127
+ return {
128
+ steps: [step],
129
+ start_article: startPage,
130
+ destination_article: targetPage,
131
+ };
132
+ return {
133
+ steps: [...prev.steps, step],
134
+ start_article: startPage,
135
+ destination_article: targetPage,
136
+ };
137
  });
138
  };
139
 
 
297
  )}
298
  </div>
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  {/* Available links */}
301
  {gameStatus === "playing" && (
302
  <>
 
367
  </div>
368
  )}
369
  </Card>
370
+ {/*
371
  <Card className="p-4 flex flex-col max-h-[500px] overflow-y-auto">
372
  <ReasoningTrace run={reasoningTrace} />
373
+ </Card> */}
374
+
375
+ <Card className="p-4 flex flex-col max-h-[500px] overflow-y-auto">
376
+ <iframe
377
+ src={`https://simple.wikipedia.org/wiki/${currentPage.replace(
378
+ /\s+/g,
379
+ "_"
380
+ )}`}
381
+ className="w-full h-full"
382
+ />
383
  </Card>
384
 
385
  {/* Right pane - Game stats and graph */}
386
+ {/* <Card className="p-4 flex flex-col overflow-y-auto">
387
  <div className="flex-1 bg-muted/30 rounded-md p-4">
388
  <div className="flex items-center gap-2 text-sm font-medium text-muted-foreground mb-2">
389
  <BarChart className="h-4 w-4" /> Path Visualization
390
  </div>
391
 
392
+ <div className="h-[500px]">
393
+ <ForceDirectedGraph runs={runs} runId={0} />
394
+ </div>
395
  </div>
396
+ </Card> */}
397
  </div>
398
  );
399
  }
 
src/components/play-tab.tsx CHANGED
@@ -6,8 +6,6 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
6
  import { Input } from "@/components/ui/input";
7
  import { Label } from "@/components/ui/label";
8
  import { Button } from "@/components/ui/button";
9
- import { Sun } from "lucide-react";
10
- import { Wifi, WifiOff } from "lucide-react";
11
  import GameComponent from "@/components/game-component";
12
  import {
13
  Select,
@@ -18,8 +16,9 @@ import {
18
  SelectTrigger,
19
  SelectValue,
20
  } from "@/components/ui/select";
 
 
21
 
22
- const API_BASE = "";
23
 
24
  export default function PlayTab() {
25
  const [player, setPlayer] = useState<"me" | "model">("me");
@@ -33,7 +32,7 @@ export default function PlayTab() {
33
  const [maxLinks, setMaxLinks] = useState<number>(200);
34
  const [isServerConnected, setIsServerConnected] = useState<boolean>(false);
35
  const [modelList, setModelList] = useState<{id: string, name: string, author: string, likes: number, trendingScore: number}[]>([]);
36
-
37
  // Server connection check
38
  useEffect(() => {
39
  fetchAvailableModels();
@@ -53,6 +52,15 @@ export default function PlayTab() {
53
  return () => clearInterval(interval);
54
  }, []);
55
 
 
 
 
 
 
 
 
 
 
56
  const handleStartGame = () => {
57
  setIsGameStarted(true);
58
  };
@@ -86,8 +94,6 @@ export default function PlayTab() {
86
  <div className="space-y-4">
87
  {!isGameStarted ? (
88
  <Card className="p-6">
89
-
90
-
91
  <div className="grid grid-cols-2 md:grid-cols-3 gap-6 mt-6">
92
  <div>
93
  <Label htmlFor="player-select" className="block mb-2">
@@ -109,27 +115,24 @@ export default function PlayTab() {
109
  <Label htmlFor="start-page" className="block mb-2">
110
  Start Page
111
  </Label>
112
- <Input
113
- id="start-page"
114
  value={startPage}
115
- onChange={(e) => setStartPage(e.target.value)}
116
- placeholder="e.g. Dogs"
117
  />
118
  </div>
119
 
120
  <div className="flex items-end gap-2">
121
  <div className="flex-1">
122
- <Label
123
- htmlFor="target-page"
124
- className="flex items-center gap-1 mb-2"
125
- >
126
  Target Page
127
  </Label>
128
- <Input
129
- id="target-page"
130
  value={targetPage}
131
- onChange={(e) => setTargetPage(e.target.value)}
132
- placeholder="e.g. Canada"
133
  />
134
  </div>
135
  <Button onClick={handleStartGame} className="mb-0.5">
 
6
  import { Input } from "@/components/ui/input";
7
  import { Label } from "@/components/ui/label";
8
  import { Button } from "@/components/ui/button";
 
 
9
  import GameComponent from "@/components/game-component";
10
  import {
11
  Select,
 
16
  SelectTrigger,
17
  SelectValue,
18
  } from "@/components/ui/select";
19
+ import { API_BASE } from "@/lib/constants";
20
+ import { VirtualizedCombobox } from "./ui/virtualized-combobox";
21
 
 
22
 
23
  export default function PlayTab() {
24
  const [player, setPlayer] = useState<"me" | "model">("me");
 
32
  const [maxLinks, setMaxLinks] = useState<number>(200);
33
  const [isServerConnected, setIsServerConnected] = useState<boolean>(false);
34
  const [modelList, setModelList] = useState<{id: string, name: string, author: string, likes: number, trendingScore: number}[]>([]);
35
+ const [allArticles, setAllArticles] = useState<string[]>([]);
36
  // Server connection check
37
  useEffect(() => {
38
  fetchAvailableModels();
 
52
  return () => clearInterval(interval);
53
  }, []);
54
 
55
+ useEffect(() => {
56
+ const fetchAllArticles = async () => {
57
+ const response = await fetch(`${API_BASE}/get_all_articles`);
58
+ const data = await response.json();
59
+ setAllArticles(data);
60
+ };
61
+ fetchAllArticles();
62
+ }, []);
63
+
64
  const handleStartGame = () => {
65
  setIsGameStarted(true);
66
  };
 
94
  <div className="space-y-4">
95
  {!isGameStarted ? (
96
  <Card className="p-6">
 
 
97
  <div className="grid grid-cols-2 md:grid-cols-3 gap-6 mt-6">
98
  <div>
99
  <Label htmlFor="player-select" className="block mb-2">
 
115
  <Label htmlFor="start-page" className="block mb-2">
116
  Start Page
117
  </Label>
118
+ <VirtualizedCombobox
119
+ options={allArticles}
120
  value={startPage}
121
+ onValueChange={(value) => setStartPage(value)}
122
+ searchPlaceholder="e.g. Dogs"
123
  />
124
  </div>
125
 
126
  <div className="flex items-end gap-2">
127
  <div className="flex-1">
128
+ <Label htmlFor="start-page" className="block mb-2">
 
 
 
129
  Target Page
130
  </Label>
131
+ <VirtualizedCombobox
132
+ options={allArticles}
133
  value={targetPage}
134
+ onValueChange={(value) => setTargetPage(value)}
135
+ searchPlaceholder="e.g. Canada"
136
  />
137
  </div>
138
  <Button onClick={handleStartGame} className="mb-0.5">
src/components/sign-in-with-hf-button.tsx CHANGED
@@ -8,7 +8,7 @@ const REDIRECT_URI = "https://huggingfacetb-wikispeedia.hf.space";
8
  const SCOPE = "openid%20profile%20email%20inference-api";
9
  const STATE = "1234567890";
10
  const SSO_URL = `https://huggingface.co/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${SCOPE}&prompt=consent&state=${STATE}`;
11
- const API_BASE = "https://huggingface.co/oauth/token";
12
  const CLIENT_SECRET = import.meta.env.VITE_HUGGINGFACE_CLIENT_SECRET; // THIS IS UNSAFE, must fix before real deploy
13
 
14
  export const SignInWithHuggingFaceButton = () => {
@@ -37,7 +37,7 @@ export const SignInWithHuggingFaceButton = () => {
37
  // remove the code from the url
38
  window.history.replaceState({}, "", window.location.pathname);
39
  setIsLoading(true);
40
- const response = await fetch(`${API_BASE}`, {
41
  method: "POST",
42
  headers: {
43
  "Content-Type": "application/x-www-form-urlencoded",
 
8
  const SCOPE = "openid%20profile%20email%20inference-api";
9
  const STATE = "1234567890";
10
  const SSO_URL = `https://huggingface.co/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${SCOPE}&prompt=consent&state=${STATE}`;
11
+ const OAUTH_API_BASE = "https://huggingface.co/oauth/token";
12
  const CLIENT_SECRET = import.meta.env.VITE_HUGGINGFACE_CLIENT_SECRET; // THIS IS UNSAFE, must fix before real deploy
13
 
14
  export const SignInWithHuggingFaceButton = () => {
 
37
  // remove the code from the url
38
  window.history.replaceState({}, "", window.location.pathname);
39
  setIsLoading(true);
40
+ const response = await fetch(`${OAUTH_API_BASE}`, {
41
  method: "POST",
42
  headers: {
43
  "Content-Type": "application/x-www-form-urlencoded",
src/components/ui/command.tsx ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Command as CommandPrimitive } from "cmdk"
3
+ import { SearchIcon } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog"
13
+
14
+ function Command({
15
+ className,
16
+ ...props
17
+ }: React.ComponentProps<typeof CommandPrimitive>) {
18
+ return (
19
+ <CommandPrimitive
20
+ data-slot="command"
21
+ className={cn(
22
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ function CommandDialog({
31
+ title = "Command Palette",
32
+ description = "Search for a command to run...",
33
+ children,
34
+ ...props
35
+ }: React.ComponentProps<typeof Dialog> & {
36
+ title?: string
37
+ description?: string
38
+ }) {
39
+ return (
40
+ <Dialog {...props}>
41
+ <DialogHeader className="sr-only">
42
+ <DialogTitle>{title}</DialogTitle>
43
+ <DialogDescription>{description}</DialogDescription>
44
+ </DialogHeader>
45
+ <DialogContent className="overflow-hidden p-0">
46
+ <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
47
+ {children}
48
+ </Command>
49
+ </DialogContent>
50
+ </Dialog>
51
+ )
52
+ }
53
+
54
+ function CommandInput({
55
+ className,
56
+ ...props
57
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
58
+ return (
59
+ <div
60
+ data-slot="command-input-wrapper"
61
+ className="flex h-9 items-center gap-2 border-b px-3"
62
+ >
63
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
64
+ <CommandPrimitive.Input
65
+ data-slot="command-input"
66
+ className={cn(
67
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
68
+ className
69
+ )}
70
+ {...props}
71
+ />
72
+ </div>
73
+ )
74
+ }
75
+
76
+ function CommandList({
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
80
+ return (
81
+ <CommandPrimitive.List
82
+ data-slot="command-list"
83
+ className={cn(
84
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
85
+ className
86
+ )}
87
+ {...props}
88
+ />
89
+ )
90
+ }
91
+
92
+ function CommandEmpty({
93
+ ...props
94
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
95
+ return (
96
+ <CommandPrimitive.Empty
97
+ data-slot="command-empty"
98
+ className="py-6 text-center text-sm"
99
+ {...props}
100
+ />
101
+ )
102
+ }
103
+
104
+ function CommandGroup({
105
+ className,
106
+ ...props
107
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
108
+ return (
109
+ <CommandPrimitive.Group
110
+ data-slot="command-group"
111
+ className={cn(
112
+ "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ )
118
+ }
119
+
120
+ function CommandSeparator({
121
+ className,
122
+ ...props
123
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
124
+ return (
125
+ <CommandPrimitive.Separator
126
+ data-slot="command-separator"
127
+ className={cn("bg-border -mx-1 h-px", className)}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function CommandItem({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
137
+ return (
138
+ <CommandPrimitive.Item
139
+ data-slot="command-item"
140
+ className={cn(
141
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
142
+ className
143
+ )}
144
+ {...props}
145
+ />
146
+ )
147
+ }
148
+
149
+ function CommandShortcut({
150
+ className,
151
+ ...props
152
+ }: React.ComponentProps<"span">) {
153
+ return (
154
+ <span
155
+ data-slot="command-shortcut"
156
+ className={cn(
157
+ "text-muted-foreground ml-auto text-xs tracking-widest",
158
+ className
159
+ )}
160
+ {...props}
161
+ />
162
+ )
163
+ }
164
+
165
+ export {
166
+ Command,
167
+ CommandDialog,
168
+ CommandInput,
169
+ CommandList,
170
+ CommandEmpty,
171
+ CommandGroup,
172
+ CommandItem,
173
+ CommandShortcut,
174
+ CommandSeparator,
175
+ }
src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { XIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Dialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
13
+ }
14
+
15
+ function DialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
19
+ }
20
+
21
+ function DialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
25
+ }
26
+
27
+ function DialogClose({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
30
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
31
+ }
32
+
33
+ function DialogOverlay({
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37
+ return (
38
+ <DialogPrimitive.Overlay
39
+ data-slot="dialog-overlay"
40
+ className={cn(
41
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
47
+ }
48
+
49
+ function DialogContent({
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<typeof DialogPrimitive.Content>) {
54
+ return (
55
+ <DialogPortal data-slot="dialog-portal">
56
+ <DialogOverlay />
57
+ <DialogPrimitive.Content
58
+ data-slot="dialog-content"
59
+ className={cn(
60
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
61
+ className
62
+ )}
63
+ {...props}
64
+ >
65
+ {children}
66
+ <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
67
+ <XIcon />
68
+ <span className="sr-only">Close</span>
69
+ </DialogPrimitive.Close>
70
+ </DialogPrimitive.Content>
71
+ </DialogPortal>
72
+ )
73
+ }
74
+
75
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="dialog-header"
79
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
80
+ {...props}
81
+ />
82
+ )
83
+ }
84
+
85
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
86
+ return (
87
+ <div
88
+ data-slot="dialog-footer"
89
+ className={cn(
90
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
91
+ className
92
+ )}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function DialogTitle({
99
+ className,
100
+ ...props
101
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
102
+ return (
103
+ <DialogPrimitive.Title
104
+ data-slot="dialog-title"
105
+ className={cn("text-lg leading-none font-semibold", className)}
106
+ {...props}
107
+ />
108
+ )
109
+ }
110
+
111
+ function DialogDescription({
112
+ className,
113
+ ...props
114
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
115
+ return (
116
+ <DialogPrimitive.Description
117
+ data-slot="dialog-description"
118
+ className={cn("text-muted-foreground text-sm", className)}
119
+ {...props}
120
+ />
121
+ )
122
+ }
123
+
124
+ export {
125
+ Dialog,
126
+ DialogClose,
127
+ DialogContent,
128
+ DialogDescription,
129
+ DialogFooter,
130
+ DialogHeader,
131
+ DialogOverlay,
132
+ DialogPortal,
133
+ DialogTitle,
134
+ DialogTrigger,
135
+ }
src/components/ui/popover.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Popover({
7
+ ...props
8
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
9
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
10
+ }
11
+
12
+ function PopoverTrigger({
13
+ ...props
14
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
15
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
16
+ }
17
+
18
+ function PopoverContent({
19
+ className,
20
+ align = "center",
21
+ sideOffset = 4,
22
+ ...props
23
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
24
+ return (
25
+ <PopoverPrimitive.Portal>
26
+ <PopoverPrimitive.Content
27
+ data-slot="popover-content"
28
+ align={align}
29
+ sideOffset={sideOffset}
30
+ className={cn(
31
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ </PopoverPrimitive.Portal>
37
+ )
38
+ }
39
+
40
+ function PopoverAnchor({
41
+ ...props
42
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
43
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
44
+ }
45
+
46
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
src/components/ui/virtualized-combobox.tsx ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import {
3
+ Command,
4
+ CommandEmpty,
5
+ CommandGroup,
6
+ CommandInput,
7
+ CommandItem,
8
+ CommandList,
9
+ } from "@/components/ui/command";
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from "@/components/ui/popover";
15
+ import { cn } from "@/lib/utils";
16
+ import { useVirtualizer } from "@tanstack/react-virtual";
17
+ import { Check, ChevronsUpDown } from "lucide-react";
18
+ import * as React from "react";
19
+
20
+ type Option = {
21
+ value: string;
22
+ label: string;
23
+ };
24
+
25
+ interface VirtualizedCommandProps {
26
+ height: string;
27
+ options: Option[];
28
+ placeholder: string;
29
+ selectedOption: string;
30
+ onSelectOption?: (option: string) => void;
31
+ }
32
+
33
+ const VirtualizedCommand = ({
34
+ height,
35
+ options,
36
+ placeholder,
37
+ selectedOption,
38
+ onSelectOption,
39
+ }: VirtualizedCommandProps) => {
40
+ const [filteredOptions, setFilteredOptions] =
41
+ React.useState<Option[]>(options);
42
+ const [focusedIndex, setFocusedIndex] = React.useState(0);
43
+ const [isKeyboardNavActive, setIsKeyboardNavActive] = React.useState(false);
44
+
45
+ const parentRef = React.useRef(null);
46
+
47
+ const virtualizer = useVirtualizer({
48
+ count: filteredOptions.length,
49
+ getScrollElement: () => parentRef.current,
50
+ estimateSize: () => 35,
51
+ });
52
+
53
+ const virtualOptions = virtualizer.getVirtualItems();
54
+
55
+ const scrollToIndex = (index: number) => {
56
+ virtualizer.scrollToIndex(index, {
57
+ align: "center",
58
+ });
59
+ };
60
+
61
+ const handleSearch = (search: string) => {
62
+ setIsKeyboardNavActive(false);
63
+ setFilteredOptions(
64
+ options.filter((option) =>
65
+ option.value.toLowerCase().includes(search.toLowerCase() ?? [])
66
+ )
67
+ );
68
+ };
69
+
70
+ const handleKeyDown = (event: React.KeyboardEvent) => {
71
+ switch (event.key) {
72
+ case "ArrowDown": {
73
+ event.preventDefault();
74
+ setIsKeyboardNavActive(true);
75
+ setFocusedIndex((prev) => {
76
+ const newIndex =
77
+ prev === -1 ? 0 : Math.min(prev + 1, filteredOptions.length - 1);
78
+ scrollToIndex(newIndex);
79
+ return newIndex;
80
+ });
81
+ break;
82
+ }
83
+ case "ArrowUp": {
84
+ event.preventDefault();
85
+ setIsKeyboardNavActive(true);
86
+ setFocusedIndex((prev) => {
87
+ const newIndex =
88
+ prev === -1 ? filteredOptions.length - 1 : Math.max(prev - 1, 0);
89
+ scrollToIndex(newIndex);
90
+ return newIndex;
91
+ });
92
+ break;
93
+ }
94
+ case "Enter": {
95
+ event.preventDefault();
96
+ if (filteredOptions[focusedIndex]) {
97
+ onSelectOption?.(filteredOptions[focusedIndex].value);
98
+ }
99
+ break;
100
+ }
101
+ default:
102
+ break;
103
+ }
104
+ };
105
+
106
+ React.useEffect(() => {
107
+ if (selectedOption) {
108
+ const option = filteredOptions.find(
109
+ (option) => option.value === selectedOption
110
+ );
111
+ if (option) {
112
+ const index = filteredOptions.indexOf(option);
113
+ setFocusedIndex(index);
114
+ virtualizer.scrollToIndex(index, {
115
+ align: "center",
116
+ });
117
+ }
118
+ }
119
+ }, [selectedOption, filteredOptions, virtualizer]);
120
+
121
+ return (
122
+ <Command shouldFilter={false} onKeyDown={handleKeyDown}>
123
+ <CommandInput onValueChange={handleSearch} placeholder={placeholder} />
124
+ <CommandList
125
+ ref={parentRef}
126
+ style={{
127
+ height: height,
128
+ width: "100%",
129
+ overflow: "auto",
130
+ }}
131
+ onMouseDown={() => setIsKeyboardNavActive(false)}
132
+ onMouseMove={() => setIsKeyboardNavActive(false)}
133
+ >
134
+ <CommandEmpty>No item found.</CommandEmpty>
135
+ <CommandGroup>
136
+ <div
137
+ style={{
138
+ height: `${virtualizer.getTotalSize()}px`,
139
+ width: "100%",
140
+ position: "relative",
141
+ }}
142
+ >
143
+ {virtualOptions.map((virtualOption) => (
144
+ <CommandItem
145
+ key={filteredOptions[virtualOption.index].value}
146
+ disabled={isKeyboardNavActive}
147
+ className={cn(
148
+ "absolute left-0 top-0 w-full bg-transparent",
149
+ focusedIndex === virtualOption.index &&
150
+ "bg-accent text-accent-foreground",
151
+ isKeyboardNavActive &&
152
+ focusedIndex !== virtualOption.index &&
153
+ "aria-selected:bg-transparent aria-selected:text-primary"
154
+ )}
155
+ style={{
156
+ height: `${virtualOption.size}px`,
157
+ transform: `translateY(${virtualOption.start}px)`,
158
+ }}
159
+ value={filteredOptions[virtualOption.index].value}
160
+ onMouseEnter={() =>
161
+ !isKeyboardNavActive && setFocusedIndex(virtualOption.index)
162
+ }
163
+ onMouseLeave={() => !isKeyboardNavActive && setFocusedIndex(-1)}
164
+ onSelect={onSelectOption}
165
+ >
166
+ <Check
167
+ className={cn(
168
+ "mr-2 h-4 w-4",
169
+ selectedOption ===
170
+ filteredOptions[virtualOption.index].value
171
+ ? "opacity-100"
172
+ : "opacity-0"
173
+ )}
174
+ />
175
+ {filteredOptions[virtualOption.index].label}
176
+ </CommandItem>
177
+ ))}
178
+ </div>
179
+ </CommandGroup>
180
+ </CommandList>
181
+ </Command>
182
+ );
183
+ };
184
+
185
+ interface VirtualizedComboboxProps {
186
+ options: string[];
187
+ searchPlaceholder?: string;
188
+ width?: string;
189
+ height?: string;
190
+ value: string;
191
+ onValueChange: (value: string) => void;
192
+ }
193
+
194
+ export function VirtualizedCombobox({
195
+ options,
196
+ searchPlaceholder = "Search items...",
197
+ value,
198
+ onValueChange,
199
+ width = "400px",
200
+ height = "400px",
201
+ }: VirtualizedComboboxProps) {
202
+ const [open, setOpen] = React.useState(false);
203
+ const [selectedOption, setSelectedOption] = React.useState("");
204
+
205
+ return (
206
+ <Popover open={open} onOpenChange={setOpen}>
207
+ <PopoverTrigger asChild>
208
+ <Button
209
+ variant="outline"
210
+ role="combobox"
211
+ aria-expanded={open}
212
+ className="justify-between"
213
+ style={{
214
+ width: width,
215
+ }}
216
+ >
217
+ {selectedOption
218
+ ? options.find((option) => option === selectedOption)
219
+ : searchPlaceholder}
220
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
221
+ </Button>
222
+ </PopoverTrigger>
223
+ <PopoverContent className="p-0" style={{ width: width }}>
224
+ <VirtualizedCommand
225
+ height={height}
226
+ options={options.map((option) => ({ value: option, label: option }))}
227
+ placeholder={searchPlaceholder}
228
+ selectedOption={value}
229
+ onSelectOption={(currentValue) => {
230
+ setSelectedOption(
231
+ currentValue === selectedOption ? "" : currentValue
232
+ );
233
+ onValueChange(currentValue);
234
+ setOpen(false);
235
+ }}
236
+ />
237
+ </PopoverContent>
238
+ </Popover>
239
+ );
240
+ }
src/lib/constants.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export const API_BASE = import.meta.env.VITE_API_BASE || "http://localhost:8000";
2
+
3
+ console.log("API_BASE", API_BASE);
yarn.lock CHANGED
The diff for this file is too large to render. See raw diff