From 8234e89a0aeabccd40a10caa42a84bc69bc33959 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:11:44 +0000 Subject: [PATCH 01/21] Initial plan From f26e8141328c8ff7fd1b06264bc0e6f5f6c099bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:18:26 +0000 Subject: [PATCH 02/21] Add BubbleGraph component with animated bubble visualization Co-authored-by: KenOKK3003 <194575962+KenOKK3003@users.noreply.github.com> --- client/src/components/BubbleGraph.tsx | 219 ++++++++++++++++++++++++++ client/src/components/GameScreen.tsx | 41 ++--- client/src/components/WordGraph.tsx | 2 +- client/src/hooks/useTimer.ts | 2 +- 4 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 client/src/components/BubbleGraph.tsx diff --git a/client/src/components/BubbleGraph.tsx b/client/src/components/BubbleGraph.tsx new file mode 100644 index 0000000..8c3b42e --- /dev/null +++ b/client/src/components/BubbleGraph.tsx @@ -0,0 +1,219 @@ +import { useState, useEffect } from 'react'; + +interface SynonymWithDefinition { + word: string; + definition: string; +} + +interface Props { + previousWord: string | null; + currentWord: string; + targetWord: string; + synonyms: SynonymWithDefinition[]; + isLoading: boolean; + onSelectWord: (word: string) => void; +} + +export default function BubbleGraph({ + previousWord, + currentWord, + targetWord, + synonyms, + isLoading, + onSelectWord, +}: Props) { + const [animating, setAnimating] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(null); + const [lastWord, setLastWord] = useState(currentWord); + + // Reset animation state when current word changes + if (lastWord !== currentWord) { + setLastWord(currentWord); + setAnimating(true); + } + + useEffect(() => { + if (animating) { + const timer = setTimeout(() => setAnimating(false), 600); + return () => clearTimeout(timer); + } + }, [animating]); + + const handleSynonymClick = (synonym: SynonymWithDefinition, index: number) => { + if (synonym.word === 'No synonyms found' || synonym.word === 'Error loading synonyms') { + return; + } + setSelectedIndex(index); + setAnimating(true); + + // Delay the word selection to allow animation to play + setTimeout(() => { + onSelectWord(synonym.word); + setSelectedIndex(null); + }, 400); + }; + + // Calculate positions for synonym bubbles in an arc pattern + const getSynonymPosition = (index: number, total: number) => { + const arcWidth = 70; // Percentage of container width + const arcHeight = 30; // Vertical offset from bottom + const startAngle = -30; // Degrees + const endAngle = 30; // Degrees + + const angleRange = endAngle - startAngle; + const angle = startAngle + (angleRange * index) / Math.max(total - 1, 1); + const radians = (angle * Math.PI) / 180; + + const centerX = 50; + const x = centerX + Math.sin(radians) * arcWidth / 2; + const y = 70 + Math.cos(radians) * arcHeight; + + return { x, y }; + }; + + const isTargetWord = (word: string) => word.toLowerCase() === targetWord.toLowerCase(); + + return ( +
+ + {/* Lines from current word to synonyms */} + {!isLoading && synonyms.length > 0 && synonyms[0].word !== 'No synonyms found' && ( + <> + {synonyms.map((_, index) => { + const pos = getSynonymPosition(index, synonyms.length); + const opacity = selectedIndex !== null && selectedIndex !== index ? 0 : 0.3; + return ( + + ); + })} + + )} + + {/* Line from previous word to current word */} + {previousWord && ( + + )} + + {/* Gradient definition */} + + + + + + + + + {/* Previous Word Bubble (Small, Top) */} + {previousWord && ( +
+
+

{previousWord}

+
+
+ )} + + {/* Current Word Bubble (Large, Center) */} +
+
+
+

Current Word

+

{currentWord}

+
+
+
+ + {/* Loading State */} + {isLoading && ( +
+
+
+

Loading synonyms...

+
+
+ )} + + {/* Synonym Bubbles (Medium, Bottom, Arc Pattern) */} + {!isLoading && synonyms.length > 0 && ( + <> + {synonyms.map((synonym, index) => { + const pos = getSynonymPosition(index, synonyms.length); + const isDisabled = synonym.word === 'No synonyms found' || synonym.word === 'Error loading synonyms'; + const isTarget = isTargetWord(synonym.word); + const isSelected = selectedIndex === index; + const shouldHide = selectedIndex !== null && selectedIndex !== index; + + return ( +
+ +
+ ); + })} + + )} +
+ ); +} diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index 0efaa59..e14f123 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -1,4 +1,5 @@ import WordGraph from './WordGraph'; +import BubbleGraph from './BubbleGraph'; import { useEffect } from 'react'; import { useGame } from '../hooks/useGame'; import { useTimer } from '../hooks/useTimer'; @@ -53,36 +54,16 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple - {/* Current Word */} -
-
-

Current Word

-

{game.currentWord}

-
- - {/* Synonyms */} - {game.isLoading ? ( -
-
-

Loading synonyms...

-
- ) : ( -
- {game.synonyms.map((synonym, index) => ( - - ))} -
- )} + {/* Bubble Graph */} +
+ 1 ? game.path[game.path.length - 2] : null} + currentWord={game.currentWord} + targetWord={targetWord} + synonyms={game.synonyms} + isLoading={game.isLoading} + onSelectWord={game.selectWord} + />
{/* Word Relationship Graph */} diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index 4dfd602..c16a12a 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import ReactFlow, { Background, Controls, diff --git a/client/src/hooks/useTimer.ts b/client/src/hooks/useTimer.ts index 53b44ef..7aeee05 100644 --- a/client/src/hooks/useTimer.ts +++ b/client/src/hooks/useTimer.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react'; export function useTimer(isActive: boolean) { const [seconds, setSeconds] = useState(0); - const intervalRef = useRef(); + const intervalRef = useRef(undefined); useEffect(() => { if (isActive) { From 1668596947f6bf3d5a99d7b2bdca6e9837a44965 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 13:23:13 +0000 Subject: [PATCH 03/21] Address code review feedback - fix timeout cleanup and state management Co-authored-by: KenOKK3003 <194575962+KenOKK3003@users.noreply.github.com> --- client/src/components/BubbleGraph.tsx | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/client/src/components/BubbleGraph.tsx b/client/src/components/BubbleGraph.tsx index 8c3b42e..d8e000c 100644 --- a/client/src/components/BubbleGraph.tsx +++ b/client/src/components/BubbleGraph.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; interface SynonymWithDefinition { word: string; @@ -22,34 +22,34 @@ export default function BubbleGraph({ isLoading, onSelectWord, }: Props) { - const [animating, setAnimating] = useState(false); const [selectedIndex, setSelectedIndex] = useState(null); - const [lastWord, setLastWord] = useState(currentWord); - - // Reset animation state when current word changes - if (lastWord !== currentWord) { - setLastWord(currentWord); - setAnimating(true); - } + const clickTimeoutRef = useRef(undefined); + // Cleanup timeouts on unmount useEffect(() => { - if (animating) { - const timer = setTimeout(() => setAnimating(false), 600); - return () => clearTimeout(timer); - } - }, [animating]); + return () => { + if (clickTimeoutRef.current) { + clearTimeout(clickTimeoutRef.current); + } + }; + }, []); const handleSynonymClick = (synonym: SynonymWithDefinition, index: number) => { if (synonym.word === 'No synonyms found' || synonym.word === 'Error loading synonyms') { return; } setSelectedIndex(index); - setAnimating(true); + + // Clear any existing timeout + if (clickTimeoutRef.current) { + clearTimeout(clickTimeoutRef.current); + } // Delay the word selection to allow animation to play - setTimeout(() => { + clickTimeoutRef.current = window.setTimeout(() => { onSelectWord(synonym.word); setSelectedIndex(null); + clickTimeoutRef.current = undefined; }, 400); }; @@ -140,7 +140,7 @@ export default function BubbleGraph({ {/* Current Word Bubble (Large, Center) */}
Date: Sat, 17 Jan 2026 21:54:19 +0800 Subject: [PATCH 04/21] Fix main.py --- server/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/main.py b/server/main.py index 2c224cd..2f7a375 100644 --- a/server/main.py +++ b/server/main.py @@ -8,10 +8,14 @@ def create_app(): CORS(app) from routes.health import health_bp - from routes.synonyms import synonyms_bp + from routes.start import start_bp + from routes.dist import dist_bp + from routes.next import next_bp app.register_blueprint(health_bp) - app.register_blueprint(synonyms_bp, url_prefix="/api") + app.register_blueprint(start_bp, url_prefix="/api") + app.register_blueprint(dist_bp, url_prefix="/api") + app.register_blueprint(next_bp, url_prefix="/api") return app From 021a50967dad46e5577460afc2bb1b0da656addf Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sat, 17 Jan 2026 22:10:00 +0800 Subject: [PATCH 05/21] fixed server imports --- server/main.py | 2 ++ server/routes/dist.py | 27 +++++++++++++-------------- server/routes/next.py | 8 +++----- server/routes/start.py | 22 ++++++++++++---------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/server/main.py b/server/main.py index 2f7a375..f102d47 100644 --- a/server/main.py +++ b/server/main.py @@ -3,6 +3,7 @@ from services.game import Game + def create_app(): app = Flask(__name__) CORS(app) @@ -19,6 +20,7 @@ def create_app(): return app + app = create_app() # In-memory game store diff --git a/server/routes/dist.py b/server/routes/dist.py index 104435d..d44db6f 100644 --- a/server/routes/dist.py +++ b/server/routes/dist.py @@ -1,32 +1,31 @@ -import uuid -import random from flask import Blueprint, jsonify, request from services.game import Game +from server.main import GAMES dist_bp = Blueprint("dist", __name__) + @dist_bp.route("/dist", methods=["GET"]) def distance(): game_id = request.headers.get("X-Game-Id") current_word = request.headers.get("X-Current-Word") if not game_id or not current_word: - return jsonify({ - "error": "X-Game-Id and X-Current-Word headers required" - }), 400 + return jsonify({"error": "X-Game-Id and X-Current-Word headers required"}), 400 game = GAMES.get(game_id) if not game: return jsonify({"error": "invalid game id"}), 404 target = game.end - dist = game.shortest_distance(current_word, target) - - return jsonify({ - "currentWord": current_word, - "targetWord": target, - "distance": dist, - "reachable": dist is not None - }) - + dist = game.shortest_path(game.graph.nodes[current_word]) + + return jsonify( + { + "currentWord": current_word, + "targetWord": target, + "distance": dist, + "reachable": dist is not None, + } + ) diff --git a/server/routes/next.py b/server/routes/next.py index ed7537c..0227e8e 100644 --- a/server/routes/next.py +++ b/server/routes/next.py @@ -1,14 +1,12 @@ -import json -import uuid -import random from flask import Blueprint, jsonify, request from services.game import Game +from server.main import GAMES -dist_bp = Blueprint("dist", __name__) +next_bp = Blueprint("next", __name__) -@dist_bp.route("/next", methods=["GET"]) +@next_bp.route("/next", methods=["GET"]) def curr(): game_id = request.headers.get("X-Game-Id") current_word = request.headers.get("X-Current-Word") diff --git a/server/routes/start.py b/server/routes/start.py index 4fb7744..3ab310d 100644 --- a/server/routes/start.py +++ b/server/routes/start.py @@ -1,24 +1,26 @@ import uuid -import random -from flask import Blueprint, jsonify, request +from flask import Blueprint, jsonify from services.game import Game +from server.main import GAMES start_bp = Blueprint("start", __name__) + @start_bp.route("/start", methods=["POST"]) def start_game(): game_id = str(uuid.uuid4()) game = Game() - game.play(None, steps=10, min_path_length=4) + # game.play(None, steps=10, min_path_length=4) GAMES[game_id] = game - return jsonify({ - "gameId": game_id, - "startWord": game.start, - "targetWord": game.end, - "optimalDistance": game.shortest_path(game.start, game.end) - }) - + return jsonify( + { + "gameId": game_id, + "startWord": game.start.word, + "targetWord": game.end.word, + "optimalDistance": game.shortest_path(game.start), + } + ) From 5fcc5d19f5d50705be0587d154b39800ca9eb86b Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sat, 17 Jan 2026 22:57:53 +0800 Subject: [PATCH 06/21] fixed server imports --- server/main.py | 4 ---- server/routes/dist.py | 5 ++--- server/routes/next.py | 5 ++--- server/routes/start.py | 4 ++-- server/services/game_manager.py | 20 ++++++++++++++++++++ 5 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 server/services/game_manager.py diff --git a/server/main.py b/server/main.py index f102d47..15d4506 100644 --- a/server/main.py +++ b/server/main.py @@ -1,8 +1,6 @@ from flask import Flask from flask_cors import CORS -from services.game import Game - def create_app(): app = Flask(__name__) @@ -23,8 +21,6 @@ def create_app(): app = create_app() -# In-memory game store -GAMES: dict[str, Game] = {} if __name__ == "__main__": app.run(debug=True, port=3001) diff --git a/server/routes/dist.py b/server/routes/dist.py index d44db6f..a80ad47 100644 --- a/server/routes/dist.py +++ b/server/routes/dist.py @@ -1,7 +1,6 @@ from flask import Blueprint, jsonify, request -from services.game import Game -from server.main import GAMES +from services.game_manager import GameManager dist_bp = Blueprint("dist", __name__) @@ -14,7 +13,7 @@ def distance(): if not game_id or not current_word: return jsonify({"error": "X-Game-Id and X-Current-Word headers required"}), 400 - game = GAMES.get(game_id) + game = GameManager.load_game(game_id) if not game: return jsonify({"error": "invalid game id"}), 404 diff --git a/server/routes/next.py b/server/routes/next.py index 0227e8e..e3a49d5 100644 --- a/server/routes/next.py +++ b/server/routes/next.py @@ -1,7 +1,6 @@ from flask import Blueprint, jsonify, request -from services.game import Game -from server.main import GAMES +from services.game_manager import GameManager next_bp = Blueprint("next", __name__) @@ -14,7 +13,7 @@ def curr(): if not game_id or not current_word: return jsonify({"error": "X-Game-Id and X-Current-Word headers required"}), 400 - game = GAMES.get(game_id) + game = GameManager.load_game(game_id) if not game: return jsonify({"error": "invalid game id"}), 404 diff --git a/server/routes/start.py b/server/routes/start.py index 3ab310d..685c7f4 100644 --- a/server/routes/start.py +++ b/server/routes/start.py @@ -2,7 +2,7 @@ from flask import Blueprint, jsonify from services.game import Game -from server.main import GAMES +from services.game_manager import GameManager start_bp = Blueprint("start", __name__) @@ -14,7 +14,7 @@ def start_game(): game = Game() # game.play(None, steps=10, min_path_length=4) - GAMES[game_id] = game + GameManager.save_game(game_id, game) return jsonify( { diff --git a/server/services/game_manager.py b/server/services/game_manager.py new file mode 100644 index 0000000..68824ad --- /dev/null +++ b/server/services/game_manager.py @@ -0,0 +1,20 @@ +import redis +import pickle + +# Configure Redis connection (adjust host/port as needed) +r = redis.Redis(host="localhost", port=6379, db=0) + + +class GameManager: + @staticmethod + def save_game(game_id, game, ttl=3600): + r.setex(game_id, ttl, pickle.dumps(game)) + + @staticmethod + def load_game(game_id): + data = r.get(game_id) + return pickle.loads(data) if data else None + + @staticmethod + def delete_game(game_id): + r.delete(game_id) From 5825486bc8c80c8c807e399b6358f4aad6675605 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:26:35 +0000 Subject: [PATCH 07/21] Initial plan From a8116c5ad553a0190d757cac01db6bf39ca9f59e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:33:53 +0000 Subject: [PATCH 08/21] Implement interactive word graph with all major UI enhancements Co-authored-by: KenOKK3003 <194575962+KenOKK3003@users.noreply.github.com> --- client/src/components/GameScreen.tsx | 46 +--- client/src/components/WordGraph.tsx | 317 +++++++++++++++++++++++---- client/src/hooks/useGame.ts | 130 +++++++++-- 3 files changed, 393 insertions(+), 100 deletions(-) diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index e14f123..2484235 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -1,5 +1,4 @@ import WordGraph from './WordGraph'; -import BubbleGraph from './BubbleGraph'; import { useEffect } from 'react'; import { useGame } from '../hooks/useGame'; import { useTimer } from '../hooks/useTimer'; @@ -16,7 +15,7 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple const timer = useTimer(!game.isComplete); useEffect(() => { - game.fetchSynonyms(startWord); + game.fetchWords(startWord); }, []); useEffect(() => { @@ -31,9 +30,9 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple }, [game.isComplete]); return ( -
+
{/* Header */} -
+

Player

@@ -54,44 +53,19 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple
- {/* Bubble Graph */} -
- 1 ? game.path[game.path.length - 2] : null} - currentWord={game.currentWord} - targetWord={targetWord} - synonyms={game.synonyms} - isLoading={game.isLoading} - onSelectWord={game.selectWord} - /> -
- - {/* Word Relationship Graph */} -
-

Your Journey:

+ {/* Main Game Area with Interactive Graph */} +
- - {/* Path Tracker */} -
-

Your path:

-
- {game.path.map((word, index) => ( -
- - {word} - - {index < game.path.length - 1 && ( - - )} -
- ))} -
-
); } diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index c16a12a..143937c 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -1,86 +1,321 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import ReactFlow, { Background, Controls, useNodesState, useEdgesState, + MiniMap, + Panel, } from 'reactflow'; -import type { Node, Edge } from 'reactflow'; +import type { Node, Edge, NodeMouseHandler } from 'reactflow'; import 'reactflow/dist/style.css'; +interface WordWithMetadata { + word: string; + definition: string; + type: 'synonym' | 'antonym' | 'related'; +} + interface Props { path: string[]; currentWord: string; targetWord: string; + words: WordWithMetadata[]; + proximity: number; + isLoading: boolean; + onSelectWord: (word: string) => void; + onRevertToWord: (word: string, index: number) => void; +} + +// Custom node component with tooltip +function CustomNode({ data }: { data: any }) { + const [showTooltip, setShowTooltip] = useState(false); + + return ( +
setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > +
+ {data.label} +
+ {showTooltip && data.definition && ( +
+ {data.definition} +
+
+ )} +
+ ); } -export default function WordGraph({ path, currentWord, targetWord }: Props) { +const nodeTypes = { + custom: CustomNode, +}; + +export default function WordGraph({ + path, + currentWord, + targetWord, + words, + proximity, + isLoading, + onSelectWord, + onRevertToWord +}: Props) { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); useEffect(() => { // Create nodes from path - const newNodes: Node[] = path.map((word, index) => { + const pathNodes: Node[] = path.map((word, index) => { const isStart = index === 0; const isCurrent = word === currentWord; const isTarget = word.toLowerCase() === targetWord.toLowerCase(); + let backgroundColor = '#e5e7eb'; // gray for visited + let color = '#374151'; + let borderColor = '#d1d5db'; + + if (isTarget) { + backgroundColor = '#10b981'; // green for target + color = 'white'; + borderColor = '#059669'; + } else if (isCurrent) { + backgroundColor = '#a855f7'; // purple for current + color = 'white'; + borderColor = '#9333ea'; + } else if (isStart) { + backgroundColor = '#6366f1'; // indigo for start + color = 'white'; + borderColor = '#4f46e5'; + } + return { - id: `${word}-${index}`, - data: { label: word }, - position: { x: index * 150, y: 100 }, - style: { - background: isTarget - ? '#10b981' - : isCurrent - ? '#8b5cf6' - : isStart - ? '#ec4899' - : '#e5e7eb', - color: isTarget || isCurrent || isStart ? 'white' : 'black', - border: '2px solid', - borderColor: isTarget - ? '#059669' - : isCurrent - ? '#7c3aed' - : isStart - ? '#db2777' - : '#d1d5db', - padding: 10, - borderRadius: 8, - fontWeight: 'bold', + id: `path-${word}-${index}`, + type: 'custom', + data: { + label: word, + backgroundColor, + color, + borderColor, + definition: `Click to revert to this word (${isStart ? 'start' : isTarget ? 'target' : isCurrent ? 'current' : 'visited'})`, }, + position: { x: index * 180, y: 100 }, + draggable: false, }; }); - // Create edges between consecutive words - const newEdges: Edge[] = []; + // Create next word options nodes + const nextWordNodes: Node[] = !isLoading && words.length > 0 && + words[0].word !== 'No words found' && words[0].word !== 'Error loading words' + ? words.map((wordData, index) => { + const { word, definition, type } = wordData; + const isTarget = word.toLowerCase() === targetWord.toLowerCase(); + + let backgroundColor = '#cbd5e1'; // default light gray + let color = '#1e293b'; + let borderColor = '#94a3b8'; + + if (isTarget) { + backgroundColor = '#10b981'; // green for target + color = 'white'; + borderColor = '#059669'; + } else if (type === 'synonym') { + backgroundColor = '#86efac'; // light green + color = '#14532d'; + borderColor = '#4ade80'; + } else if (type === 'antonym') { + backgroundColor = '#fca5a5'; // light red + color = '#7f1d1d'; + borderColor = '#ef4444'; + } else if (type === 'related') { + backgroundColor = '#93c5fd'; // light blue + color = '#1e3a8a'; + borderColor = '#3b82f6'; + } + + return { + id: `next-${word}-${index}`, + type: 'custom', + data: { + label: word, + backgroundColor, + color, + borderColor, + definition, + }, + position: { + x: (path.length) * 180 + (index % 3) * 180, + y: 250 + Math.floor(index / 3) * 80 + }, + draggable: false, + }; + }) + : []; + + setNodes([...pathNodes, ...nextWordNodes]); + + // Create edges + const pathEdges: Edge[] = []; for (let i = 0; i < path.length - 1; i++) { - newEdges.push({ - id: `e${i}-${i + 1}`, - source: `${path[i]}-${i}`, - target: `${path[i + 1]}-${i + 1}`, - animated: i === path.length - 2, // Animate the last edge - style: { stroke: '#8b5cf6', strokeWidth: 2 }, + pathEdges.push({ + id: `path-edge-${i}`, + source: `path-${path[i]}-${i}`, + target: `path-${path[i + 1]}-${i + 1}`, + animated: i === path.length - 2, + style: { stroke: '#8b5cf6', strokeWidth: 3 }, + type: 'smoothstep', }); } - setNodes(newNodes); - setEdges(newEdges); - }, [path, currentWord, targetWord]); + // Create edges from current word to next options + const currentNodeId = `path-${currentWord}-${path.length - 1}`; + const currentNodeExists = pathNodes.some(node => node.id === currentNodeId); + + const nextEdges: Edge[] = !isLoading && nextWordNodes.length > 0 && currentNodeExists + ? nextWordNodes.map((node, index) => ({ + id: `next-edge-${index}`, + source: currentNodeId, + target: node.id, + animated: false, + style: { stroke: '#cbd5e1', strokeWidth: 2, strokeDasharray: '5,5' }, + type: 'smoothstep', + })) + : []; + + setEdges([...pathEdges, ...nextEdges]); + }, [path, currentWord, targetWord, words, isLoading]); + + const handleNodeClick: NodeMouseHandler = (_event, node) => { + // Handle path node clicks (revert) + if (node.id.startsWith('path-')) { + const index = parseInt(node.id.split('-').pop() || '0'); + const word = path[index]; + if (index < path.length - 1) { + onRevertToWord(word, index); + } + } + + // Handle next word option clicks + if (node.id.startsWith('next-')) { + const word = node.data.label; + onSelectWord(word); + } + }; + + // Thermometer colors based on proximity + const getThermometerColor = () => { + if (proximity >= 80) return '#ef4444'; // hot red + if (proximity >= 60) return '#f97316'; // orange + if (proximity >= 40) return '#eab308'; // yellow + if (proximity >= 20) return '#3b82f6'; // blue + return '#06b6d4'; // cold cyan + }; + + const getThermometerLabel = () => { + if (proximity >= 80) return 'Very Hot! 🔥'; + if (proximity >= 60) return 'Hot 🌡️'; + if (proximity >= 40) return 'Warm ☀️'; + if (proximity >= 20) return 'Cool ❄️'; + return 'Cold 🧊'; + }; return ( -
+
- + + { + if (node.id.startsWith('path-')) return '#a855f7'; + return '#cbd5e1'; + }} + maskColor="rgba(0, 0, 0, 0.1)" + /> + + {/* Legend Panel */} + +

Legend

+
+
+ Synonym +
+
+
+ Antonym +
+
+
+ Related +
+
+
+ Visited +
+
+ + {/* Thermometer Panel */} + +
+

Proximity

+
+ {getThermometerLabel()} +
+
+
+
+
{proximity}%
+
+
+ + {/* Instructions Panel */} + +

How to play:

+
    +
  • • Click a word option to move forward
  • +
  • • Click a visited word to go back
  • +
  • • Drag to pan, scroll to zoom
  • +
  • • Hover over words for definitions
  • +
+
+ + {isLoading && ( + +
+
+ Loading words... +
+
+ )}
); diff --git a/client/src/hooks/useGame.ts b/client/src/hooks/useGame.ts index de1166f..3a70939 100644 --- a/client/src/hooks/useGame.ts +++ b/client/src/hooks/useGame.ts @@ -1,17 +1,19 @@ import { useState, useCallback } from 'react'; -interface SynonymWithDefinition { +interface WordWithMetadata { word: string; definition: string; + type: 'synonym' | 'antonym' | 'related'; } interface GameState { currentWord: string; targetWord: string; path: string[]; - synonyms: SynonymWithDefinition[]; + words: WordWithMetadata[]; isLoading: boolean; isComplete: boolean; + proximity: number; // 0-100, higher = closer to target } export function useGame(startWord: string, targetWord: string) { @@ -19,48 +21,114 @@ export function useGame(startWord: string, targetWord: string) { currentWord: startWord, targetWord, path: [startWord], - synonyms: [], + words: [], isLoading: false, isComplete: false, + proximity: 0, }); - const fetchSynonyms = useCallback(async (word: string) => { + const calculateProximity = useCallback(async (word: string, target: string): Promise => { + try { + // Check if the word is in the target's synonyms + const response = await fetch( + `https://api.datamuse.com/words?rel_syn=${target}&max=50` + ); + const data = await response.json(); + const synonyms = data.map((item: any) => item.word.toLowerCase()); + + if (word.toLowerCase() === target.toLowerCase()) { + return 100; + } + + if (synonyms.includes(word.toLowerCase())) { + return 80; + } + + // Check reverse - if target is in word's synonyms + const reverseResponse = await fetch( + `https://api.datamuse.com/words?rel_syn=${word}&max=50` + ); + const reverseData = await reverseResponse.json(); + const reverseSynonyms = reverseData.map((item: any) => item.word.toLowerCase()); + + if (reverseSynonyms.includes(target.toLowerCase())) { + return 70; + } + + // Default based on path length - shorter path = better + return Math.max(0, 50 - state.path.length * 5); + } catch { + return 50; + } + }, [state.path.length]); + + const fetchWords = useCallback(async (word: string) => { setState(prev => ({ ...prev, isLoading: true })); try { - // Fetch synonyms - const synonymResponse = await fetch( - `https://api.datamuse.com/words?rel_syn=${word}&max=8&md=d` - ); - const synonymData = await synonymResponse.json(); + // Fetch synonyms, antonyms, and related words in parallel + const [synonymResponse, antonymResponse, relatedResponse] = await Promise.all([ + fetch(`https://api.datamuse.com/words?rel_syn=${word}&max=4&md=d`), + fetch(`https://api.datamuse.com/words?rel_ant=${word}&max=2&md=d`), + fetch(`https://api.datamuse.com/words?rel_trg=${word}&max=2&md=d`), + ]); + + const [synonymData, antonymData, relatedData] = await Promise.all([ + synonymResponse.json(), + antonymResponse.json(), + relatedResponse.json(), + ]); + + // Map each type with metadata + const synonyms: WordWithMetadata[] = synonymData.map((item: any) => ({ + word: item.word, + definition: item.defs && item.defs.length > 0 + ? item.defs[0].replace(/^\w+\t/, '') + : 'No definition available', + type: 'synonym' as const, + })); - // Map to include definitions (if available) - const synonymsWithDefs: SynonymWithDefinition[] = synonymData.map((item: any) => ({ + const antonyms: WordWithMetadata[] = antonymData.map((item: any) => ({ word: item.word, definition: item.defs && item.defs.length > 0 - ? item.defs[0].replace(/^\w+\t/, '') // Remove part of speech prefix - : 'No definition available' + ? item.defs[0].replace(/^\w+\t/, '') + : 'No definition available', + type: 'antonym' as const, })); + const related: WordWithMetadata[] = relatedData.map((item: any) => ({ + word: item.word, + definition: item.defs && item.defs.length > 0 + ? item.defs[0].replace(/^\w+\t/, '') + : 'No definition available', + type: 'related' as const, + })); + + const allWords = [...synonyms, ...antonyms, ...related]; + + // Calculate proximity to target + const proximity = await calculateProximity(word, state.targetWord); + setState(prev => ({ ...prev, - synonyms: synonymsWithDefs.length > 0 - ? synonymsWithDefs - : [{ word: 'No synonyms found', definition: '' }], + words: allWords.length > 0 + ? allWords + : [{ word: 'No words found', definition: '', type: 'synonym' }], isLoading: false, + proximity, })); } catch (error) { - console.error('Failed to fetch synonyms:', error); + console.error('Failed to fetch words:', error); setState(prev => ({ ...prev, - synonyms: [{ word: 'Error loading synonyms', definition: '' }], + words: [{ word: 'Error loading words', definition: '', type: 'synonym' }], isLoading: false })); } - }, []); + }, [state.targetWord, calculateProximity]); const selectWord = useCallback((word: string) => { - if (word === 'No synonyms found' || word === 'Error loading synonyms') { + if (word === 'No words found' || word === 'Error loading words') { return; } @@ -77,13 +145,29 @@ export function useGame(startWord: string, targetWord: string) { }); if (word.toLowerCase() !== state.targetWord.toLowerCase()) { - fetchSynonyms(word); + fetchWords(word); } - }, [state.targetWord, fetchSynonyms]); + }, [state.targetWord, fetchWords]); + + const revertToWord = useCallback((word: string, index: number) => { + setState(prev => { + const newPath = prev.path.slice(0, index + 1); + + return { + ...prev, + currentWord: word, + path: newPath, + isComplete: false, + }; + }); + + fetchWords(word); + }, [fetchWords]); return { ...state, - fetchSynonyms, + fetchWords, selectWord, + revertToWord, }; } From 9f2f19af3d5aa8a89122a662e89699f7a3bc6bf6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:36:54 +0000 Subject: [PATCH 09/21] Fix TypeScript and ESLint issues Co-authored-by: KenOKK3003 <194575962+KenOKK3003@users.noreply.github.com> --- client/src/components/GameScreen.tsx | 11 ++++++++++- client/src/components/WordGraph.tsx | 4 ++-- client/src/hooks/useGame.ts | 25 +++++++++++++++---------- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index 2484235..bc938cc 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -3,11 +3,18 @@ import { useEffect } from 'react'; import { useGame } from '../hooks/useGame'; import { useTimer } from '../hooks/useTimer'; +interface GameResult { + playerName: string; + path: string[]; + moves: number; + timeSeconds: number; +} + interface Props { startWord: string; targetWord: string; playerName: string; - onComplete: (result: any) => void; + onComplete: (result: GameResult) => void; } export default function GameScreen({ startWord, targetWord, playerName, onComplete }: Props) { @@ -16,6 +23,7 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple useEffect(() => { game.fetchWords(startWord); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -27,6 +35,7 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple timeSeconds: timer.seconds, }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [game.isComplete]); return ( diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index 143937c..08addfe 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -28,7 +28,7 @@ interface Props { } // Custom node component with tooltip -function CustomNode({ data }: { data: any }) { +function CustomNode({ data }: { data: { label: string; backgroundColor: string; color: string; borderColor: string; definition?: string } }) { const [showTooltip, setShowTooltip] = useState(false); return ( @@ -193,7 +193,7 @@ export default function WordGraph({ : []; setEdges([...pathEdges, ...nextEdges]); - }, [path, currentWord, targetWord, words, isLoading]); + }, [path, currentWord, targetWord, words, isLoading, setNodes, setEdges]); const handleNodeClick: NodeMouseHandler = (_event, node) => { // Handle path node clicks (revert) diff --git a/client/src/hooks/useGame.ts b/client/src/hooks/useGame.ts index 3a70939..49a3516 100644 --- a/client/src/hooks/useGame.ts +++ b/client/src/hooks/useGame.ts @@ -16,6 +16,11 @@ interface GameState { proximity: number; // 0-100, higher = closer to target } +interface DatamuseWord { + word: string; + defs?: string[]; +} + export function useGame(startWord: string, targetWord: string) { const [state, setState] = useState({ currentWord: startWord, @@ -33,8 +38,8 @@ export function useGame(startWord: string, targetWord: string) { const response = await fetch( `https://api.datamuse.com/words?rel_syn=${target}&max=50` ); - const data = await response.json(); - const synonyms = data.map((item: any) => item.word.toLowerCase()); + const data: DatamuseWord[] = await response.json(); + const synonyms = data.map((item) => item.word.toLowerCase()); if (word.toLowerCase() === target.toLowerCase()) { return 100; @@ -48,8 +53,8 @@ export function useGame(startWord: string, targetWord: string) { const reverseResponse = await fetch( `https://api.datamuse.com/words?rel_syn=${word}&max=50` ); - const reverseData = await reverseResponse.json(); - const reverseSynonyms = reverseData.map((item: any) => item.word.toLowerCase()); + const reverseData: DatamuseWord[] = await reverseResponse.json(); + const reverseSynonyms = reverseData.map((item) => item.word.toLowerCase()); if (reverseSynonyms.includes(target.toLowerCase())) { return 70; @@ -74,13 +79,13 @@ export function useGame(startWord: string, targetWord: string) { ]); const [synonymData, antonymData, relatedData] = await Promise.all([ - synonymResponse.json(), - antonymResponse.json(), - relatedResponse.json(), + synonymResponse.json() as Promise, + antonymResponse.json() as Promise, + relatedResponse.json() as Promise, ]); // Map each type with metadata - const synonyms: WordWithMetadata[] = synonymData.map((item: any) => ({ + const synonyms: WordWithMetadata[] = synonymData.map((item) => ({ word: item.word, definition: item.defs && item.defs.length > 0 ? item.defs[0].replace(/^\w+\t/, '') @@ -88,7 +93,7 @@ export function useGame(startWord: string, targetWord: string) { type: 'synonym' as const, })); - const antonyms: WordWithMetadata[] = antonymData.map((item: any) => ({ + const antonyms: WordWithMetadata[] = antonymData.map((item) => ({ word: item.word, definition: item.defs && item.defs.length > 0 ? item.defs[0].replace(/^\w+\t/, '') @@ -96,7 +101,7 @@ export function useGame(startWord: string, targetWord: string) { type: 'antonym' as const, })); - const related: WordWithMetadata[] = relatedData.map((item: any) => ({ + const related: WordWithMetadata[] = relatedData.map((item) => ({ word: item.word, definition: item.defs && item.defs.length > 0 ? item.defs[0].replace(/^\w+\t/, '') From 264fac45904faaff50e53f6013f0e7c7374782eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:39:57 +0000 Subject: [PATCH 10/21] Address code review feedback: improve type safety and code quality Co-authored-by: KenOKK3003 <194575962+KenOKK3003@users.noreply.github.com> --- client/src/components/GameScreen.tsx | 4 ++-- client/src/components/WordGraph.tsx | 25 ++++++++++++++++--------- client/src/hooks/useGame.ts | 20 ++++++++++++-------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index bc938cc..c666bf3 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -23,7 +23,7 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple useEffect(() => { game.fetchWords(startWord); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps -- Only run on mount to initialize game }, []); useEffect(() => { @@ -35,7 +35,7 @@ export default function GameScreen({ startWord, targetWord, playerName, onComple timeSeconds: timer.seconds, }); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps -- Only trigger on completion status change }, [game.isComplete]); return ( diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index 08addfe..55ad761 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -8,14 +8,9 @@ import ReactFlow, { Panel, } from 'reactflow'; import type { Node, Edge, NodeMouseHandler } from 'reactflow'; +import type { WordWithMetadata } from '../hooks/useGame'; import 'reactflow/dist/style.css'; -interface WordWithMetadata { - word: string; - definition: string; - type: 'synonym' | 'antonym' | 'related'; -} - interface Props { path: string[]; currentWord: string; @@ -28,7 +23,17 @@ interface Props { } // Custom node component with tooltip -function CustomNode({ data }: { data: { label: string; backgroundColor: string; color: string; borderColor: string; definition?: string } }) { +function CustomNode({ data }: { + data: { + label: string; + backgroundColor: string; + color: string; + borderColor: string; + definition?: string; + pathIndex?: number; + isPathNode?: boolean; + } +}) { const [showTooltip, setShowTooltip] = useState(false); return ( @@ -108,6 +113,8 @@ export default function WordGraph({ color, borderColor, definition: `Click to revert to this word (${isStart ? 'start' : isTarget ? 'target' : isCurrent ? 'current' : 'visited'})`, + pathIndex: index, + isPathNode: true, }, position: { x: index * 180, y: 100 }, draggable: false, @@ -197,8 +204,8 @@ export default function WordGraph({ const handleNodeClick: NodeMouseHandler = (_event, node) => { // Handle path node clicks (revert) - if (node.id.startsWith('path-')) { - const index = parseInt(node.id.split('-').pop() || '0'); + if (node.data.isPathNode && typeof node.data.pathIndex === 'number') { + const index = node.data.pathIndex; const word = path[index]; if (index < path.length - 1) { onRevertToWord(word, index); diff --git a/client/src/hooks/useGame.ts b/client/src/hooks/useGame.ts index 49a3516..8211a56 100644 --- a/client/src/hooks/useGame.ts +++ b/client/src/hooks/useGame.ts @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; -interface WordWithMetadata { +export interface WordWithMetadata { word: string; definition: string; type: 'synonym' | 'antonym' | 'related'; @@ -33,10 +33,14 @@ export function useGame(startWord: string, targetWord: string) { }); const calculateProximity = useCallback(async (word: string, target: string): Promise => { + const MAX_SYNONYMS_TO_CHECK = 50; + const DEFAULT_PROXIMITY_BASE = 50; + const PROXIMITY_PENALTY_PER_MOVE = 5; + try { // Check if the word is in the target's synonyms const response = await fetch( - `https://api.datamuse.com/words?rel_syn=${target}&max=50` + `https://api.datamuse.com/words?rel_syn=${target}&max=${MAX_SYNONYMS_TO_CHECK}` ); const data: DatamuseWord[] = await response.json(); const synonyms = data.map((item) => item.word.toLowerCase()); @@ -51,7 +55,7 @@ export function useGame(startWord: string, targetWord: string) { // Check reverse - if target is in word's synonyms const reverseResponse = await fetch( - `https://api.datamuse.com/words?rel_syn=${word}&max=50` + `https://api.datamuse.com/words?rel_syn=${word}&max=${MAX_SYNONYMS_TO_CHECK}` ); const reverseData: DatamuseWord[] = await reverseResponse.json(); const reverseSynonyms = reverseData.map((item) => item.word.toLowerCase()); @@ -61,7 +65,7 @@ export function useGame(startWord: string, targetWord: string) { } // Default based on path length - shorter path = better - return Math.max(0, 50 - state.path.length * 5); + return Math.max(0, DEFAULT_PROXIMITY_BASE - state.path.length * PROXIMITY_PENALTY_PER_MOVE); } catch { return 50; } @@ -78,10 +82,10 @@ export function useGame(startWord: string, targetWord: string) { fetch(`https://api.datamuse.com/words?rel_trg=${word}&max=2&md=d`), ]); - const [synonymData, antonymData, relatedData] = await Promise.all([ - synonymResponse.json() as Promise, - antonymResponse.json() as Promise, - relatedResponse.json() as Promise, + const [synonymData, antonymData, relatedData]: [DatamuseWord[], DatamuseWord[], DatamuseWord[]] = await Promise.all([ + synonymResponse.json(), + antonymResponse.json(), + relatedResponse.json(), ]); // Map each type with metadata From 4d54d7918264de6e66b39927c7ef7d85d724e7a1 Mon Sep 17 00:00:00 2001 From: Kenneth Ong Date: Sun, 18 Jan 2026 02:50:36 +0800 Subject: [PATCH 11/21] graph nodes --- .python-version | 1 + client/src/components/WordGraph.tsx | 460 +++++++++++++++++++++++----- main.py | 6 + package-lock.json | 6 + pyproject.toml | 11 + server/state.py | 1 + uv.lock | 174 +++++++++++ 7 files changed, 589 insertions(+), 70 deletions(-) create mode 100644 .python-version create mode 100644 main.py create mode 100644 package-lock.json create mode 100644 pyproject.toml create mode 100644 server/state.py create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index 55ad761..5d50d2b 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import ReactFlow, { Background, Controls, @@ -22,6 +22,13 @@ interface Props { onRevertToWord: (word: string, index: number) => void; } +interface HistoricalNode { + word: string; + position: { x: number; y: number }; + definition?: string; + type?: string; +} + // Custom node component with tooltip function CustomNode({ data }: { data: { @@ -29,9 +36,10 @@ function CustomNode({ data }: { backgroundColor: string; color: string; borderColor: string; - definition?: string; - pathIndex?: number; - isPathNode?: boolean; + definition?: string; + pathIndex?: number; + isPathNode?: boolean; + isHistorical?: boolean; } }) { const [showTooltip, setShowTooltip] = useState(false); @@ -48,9 +56,10 @@ function CustomNode({ data }: { backgroundColor: data.backgroundColor, color: data.color, borderColor: data.borderColor, + opacity: data.isHistorical ? 0.5 : 1, }} > - {data.label} + {data. label}
{showTooltip && data.definition && (
@@ -63,7 +72,7 @@ function CustomNode({ data }: { } const nodeTypes = { - custom: CustomNode, + custom: CustomNode, }; export default function WordGraph({ @@ -78,13 +87,77 @@ export default function WordGraph({ }: Props) { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + // Store positions of all words to preserve them when selected + const wordPositionsRef = useRef>(new Map()); + + // Store all historical option nodes that have been shown + const historicalNodesRef = useRef>(new Map()); useEffect(() => { + // Collision detection helper + const MIN_DISTANCE = 120; // Minimum distance between nodes to prevent overlap + + const checkCollision = (pos: { x: number; y: number }, existingPositions: Array<{ x: number; y: number }>) => { + for (const existing of existingPositions) { + const distance = Math.sqrt( + Math.pow(pos.x - existing.x, 2) + Math.pow(pos.y - existing.y, 2) + ); + if (distance < MIN_DISTANCE) { + return true; + } + } + return false; + }; + + // Adjust position to avoid collisions + const findNonCollidingPosition = ( + basePos: { x: number; y: number }, + centerX: number, + centerY: number, + existingPositions: Array<{ x: number; y: number }>, + baseAngle?: number + ) => { + let position = { ... basePos }; + let attempts = 0; + const maxAttempts = 30; + + while (checkCollision(position, existingPositions) && attempts < maxAttempts) { + attempts++; + + // Calculate current distance from center + const distance = Math.sqrt( + Math.pow(position.x - centerX, 2) + Math.pow(position.y - centerY, 2) + ); + + // If baseAngle is provided, use it; otherwise calculate from current position + let angle = baseAngle; + if (angle === undefined) { + angle = Math.atan2(position. y - centerY, position.x - centerX) * (180 / Math.PI); + } + + // Try increasing radius + const newDistance = distance + 30; // Increase by 30px each attempt + + // Add slight angle variation to spread out + const angleVariation = (attempts % 2 === 0 ? 1 : -1) * (attempts * 5); + const adjustedAngle = angle + angleVariation; + const radian = (adjustedAngle * Math.PI) / 180; + + position = { + x: centerX + Math.cos(radian) * newDistance, + y: centerY + Math.sin(radian) * newDistance, + }; + } + + return position; + }; + // Create nodes from path - const pathNodes: Node[] = path.map((word, index) => { + const pathNodes: Node[] = path.map((word, index) => { const isStart = index === 0; const isCurrent = word === currentWord; - const isTarget = word.toLowerCase() === targetWord.toLowerCase(); + const isTarget = word. toLowerCase() === targetWord.toLowerCase(); let backgroundColor = '#e5e7eb'; // gray for visited let color = '#374151'; @@ -104,6 +177,19 @@ export default function WordGraph({ borderColor = '#4f46e5'; } + // Check if this word has a saved position (was previously a next word option) + const savedPosition = wordPositionsRef.current.get(word.toLowerCase()); + let position; + + if (savedPosition) { + // Use the saved position from when it was a next word option + position = savedPosition; + } else { + // Default linear positioning for the start word or words without saved positions + position = { x: index * 200, y: 100 }; + wordPositionsRef.current.set(word.toLowerCase(), position); + } + return { id: `path-${word}-${index}`, type: 'custom', @@ -112,97 +198,316 @@ export default function WordGraph({ backgroundColor, color, borderColor, - definition: `Click to revert to this word (${isStart ? 'start' : isTarget ? 'target' : isCurrent ? 'current' : 'visited'})`, + definition: `Click to revert to this word (${isStart ? 'start' : isTarget ? 'target' : isCurrent ? 'current' : 'visited'})`, pathIndex: index, isPathNode: true, }, - position: { x: index * 180, y: 100 }, + position, draggable: false, }; }); - // Create next word options nodes - const nextWordNodes: Node[] = !isLoading && words.length > 0 && - words[0].word !== 'No words found' && words[0].word !== 'Error loading words' - ? words.map((wordData, index) => { - const { word, definition, type } = wordData; - const isTarget = word.toLowerCase() === targetWord.toLowerCase(); + // Group words by type + const synonyms = words.filter(w => w.type === 'synonym'); + const antonyms = words. filter(w => w.type === 'antonym'); + const related = words.filter(w => w.type === 'related'); + const other = words.filter(w => ! w.type || (w.type !== 'synonym' && w.type !== 'antonym' && w.type !== 'related')); + + // Create next word options nodes with circular positioning + const nextWordNodes: Node[] = []; + const currentWordSet = new Set(words.map(w => w.word.toLowerCase())); + + if (! isLoading && words.length > 0 && + words[0].word !== 'No words found' && words[0].word !== 'Error loading words') { + + // Get existing positions from path nodes + const existingPositions = pathNodes.map(node => node.position); + + // Get the current word's position + const currentNodePosition = wordPositionsRef.current.get(currentWord.toLowerCase()) || + { x: (path. length - 1) * 200, y: 100 }; + + const currentX = currentNodePosition.x; + const currentY = currentNodePosition.y; + const baseRadius = 250; // Increased base distance from current node + const radiusIncrement = 90; // Space between layers + + // Helper function to calculate circular position + const getCircularPosition = (angle: number, distance: number) => { + const radian = (angle * Math. PI) / 180; + return { + x: currentX + Math.cos(radian) * distance, + y: currentY + Math.sin(radian) * distance, + }; + }; + + // Helper function to distribute nodes in an angular range with proper spacing + const distributeNodesInSection = ( + nodeCount: number, + startAngle: number, + angleRange: number, + baseRadius: number + ) => { + const positions: Array<{ x: number; y: number; layer: number; angle: number }> = []; + const maxNodesPerLayer = 4; // Maximum nodes per radius layer to avoid overlap + + for (let i = 0; i < nodeCount; i++) { + const layer = Math.floor(i / maxNodesPerLayer); + const indexInLayer = i % maxNodesPerLayer; + const nodesInThisLayer = Math.min(nodeCount - layer * maxNodesPerLayer, maxNodesPerLayer); - let backgroundColor = '#cbd5e1'; // default light gray - let color = '#1e293b'; - let borderColor = '#94a3b8'; - - if (isTarget) { - backgroundColor = '#10b981'; // green for target - color = 'white'; - borderColor = '#059669'; - } else if (type === 'synonym') { - backgroundColor = '#86efac'; // light green - color = '#14532d'; - borderColor = '#4ade80'; - } else if (type === 'antonym') { - backgroundColor = '#fca5a5'; // light red - color = '#7f1d1d'; - borderColor = '#ef4444'; - } else if (type === 'related') { - backgroundColor = '#93c5fd'; // light blue - color = '#1e3a8a'; - borderColor = '#3b82f6'; - } - - return { - id: `next-${word}-${index}`, - type: 'custom', - data: { - label: word, - backgroundColor, - color, - borderColor, - definition, - }, - position: { - x: (path.length) * 180 + (index % 3) * 180, - y: 250 + Math.floor(index / 3) * 80 - }, - draggable: false, - }; - }) - : []; + // Calculate angle for this node within its layer + const angleStep = angleRange / Math.max(nodesInThisLayer, 1); + const angle = startAngle + (indexInLayer * angleStep) + (angleStep / 2) - (angleRange / 2); + + // Calculate distance with layer offset + const distance = baseRadius + (layer * radiusIncrement); + + const position = getCircularPosition(angle, distance); + positions.push({ ... position, layer, angle }); + } + + return positions; + }; + + // Track all new node positions to check for collisions + const allNewPositions: Array<{ x: number; y: number }> = []; + + // Helper function to create node + const createWordNode = ( + wordData: WordWithMetadata, + index: number, + basePosition: { x: number; y: number; angle: number }, + currentX: number, + currentY: number + ) => { + const { word, definition, type } = wordData; + const isTarget = word.toLowerCase() === targetWord.toLowerCase(); + + let backgroundColor = '#cbd5e1'; + let color = '#1e293b'; + let borderColor = '#94a3b8'; + + if (isTarget) { + backgroundColor = '#10b981'; + color = 'white'; + borderColor = '#059669'; + } else if (type === 'synonym') { + backgroundColor = '#86efac'; + color = '#14532d'; + borderColor = '#4ade80'; + } else if (type === 'antonym') { + backgroundColor = '#fca5a5'; + color = '#7f1d1d'; + borderColor = '#ef4444'; + } else if (type === 'related') { + backgroundColor = '#93c5fd'; + color = '#1e3a8a'; + borderColor = '#3b82f6'; + } + + const position = findNonCollidingPosition( + basePosition, + currentX, + currentY, + [... existingPositions, ...allNewPositions], + basePosition.angle + ); + + allNewPositions.push(position); + + // Save this position for future reference + if (! wordPositionsRef.current.has(word.toLowerCase())) { + wordPositionsRef.current. set(word.toLowerCase(), { x: position.x, y: position.y }); + } + + // Add to historical nodes + historicalNodesRef.current.set(word.toLowerCase(), { + word, + position: { x: position.x, y: position.y }, + definition, + type, + }); - setNodes([...pathNodes, ...nextWordNodes]); + return { + id: `next-${word}-${nextWordNodes.length}`, + type: 'custom', + data: { + label: word, + backgroundColor, + color, + borderColor, + definition: definition || type || 'Word option', + }, + position: { x: position.x, y: position.y }, + draggable: false, + }; + }; + + // Synonyms spawn in top section (-90° centered, 100° range) + const synonymPositions = distributeNodesInSection( + synonyms.length, + -90, + 100, + baseRadius + ); + + synonyms.forEach((wordData, index) => { + const node = createWordNode(wordData, index, synonymPositions[index], currentX, currentY); + nextWordNodes.push(node); + }); + + // Antonyms spawn in bottom-right section (60° centered, 100° range) + const antonymPositions = distributeNodesInSection( + antonyms.length, + 60, + 100, + baseRadius + ); + + antonyms. forEach((wordData, index) => { + const node = createWordNode(wordData, index, antonymPositions[index], currentX, currentY); + nextWordNodes.push(node); + }); + + // Related words spawn in left section (180° centered, 100° range) + const relatedPositions = distributeNodesInSection( + related.length, + 180, + 100, + baseRadius + ); + + related.forEach((wordData, index) => { + const node = createWordNode(wordData, index, relatedPositions[index], currentX, currentY); + nextWordNodes.push(node); + }); + + // Other words + const otherPositions = distributeNodesInSection( + other.length, + -135, + 80, + baseRadius + ); + + other.forEach((wordData, index) => { + const node = createWordNode(wordData, index, otherPositions[index], currentX, currentY); + nextWordNodes. push(node); + }); + } + + // Collect all occupied positions (path + current options) + const allOccupiedPositions: Array<{ x: number; y: number }> = [ + ...pathNodes.map(node => node.position), + ...nextWordNodes.map(node => node.position), + ]; - // Create edges - const pathEdges: Edge[] = []; + // Add historical nodes that are not in current path or current options + const pathWordSet = new Set(path.map(w => w.toLowerCase())); + const historicalNodes: Node[] = []; + + // Get the current word's position for calculating adjustments + const currentNodePosition = wordPositionsRef.current.get(currentWord.toLowerCase()) || + { x: (path.length - 1) * 200, y: 100 }; + + historicalNodesRef.current.forEach((historicalNode, wordKey) => { + // Skip if this word is in the current path or current word options + if (! pathWordSet.has(wordKey) && !currentWordSet.has(wordKey)) { + // Check if the historical node's stored position would collide + let finalPosition = historicalNode.position; + + if (checkCollision(historicalNode.position, allOccupiedPositions)) { + // Find a non-colliding position near the original + finalPosition = findNonCollidingPosition( + historicalNode. position, + currentNodePosition. x, + currentNodePosition. y, + allOccupiedPositions, + undefined // Let it calculate angle from position + ); + + // Update the stored position + historicalNodesRef.current.set(wordKey, { + ...historicalNode, + position: finalPosition, + }); + } + + // Add to occupied positions so subsequent historical nodes avoid it + allOccupiedPositions.push(finalPosition); + + historicalNodes.push({ + id: `historical-${historicalNode.word}`, + type: 'custom', + data: { + label: historicalNode. word, + backgroundColor: '#9ca3af', // gray + color: '#374151', + borderColor: '#6b7280', + definition: historicalNode.definition || 'Previously shown option', + isHistorical: true, + }, + position: finalPosition, + draggable: false, + }); + } + }); + + setNodes([...pathNodes, ...nextWordNodes, ...historicalNodes]); + + // Create edges connecting all selected path nodes + const pathEdges: Edge[] = []; for (let i = 0; i < path.length - 1; i++) { pathEdges.push({ id: `path-edge-${i}`, source: `path-${path[i]}-${i}`, target: `path-${path[i + 1]}-${i + 1}`, - animated: i === path.length - 2, - style: { stroke: '#8b5cf6', strokeWidth: 3 }, + animated: i === path.length - 2, // Animate the most recent connection + style: { stroke: '#8b5cf6', strokeWidth: 3 }, type: 'smoothstep', }); } - // Create edges from current word to next options + // Create edges from current word to all next word options (not historical) const currentNodeId = `path-${currentWord}-${path.length - 1}`; const currentNodeExists = pathNodes.some(node => node.id === currentNodeId); - const nextEdges: Edge[] = !isLoading && nextWordNodes.length > 0 && currentNodeExists + const nextEdges: Edge[] = ! isLoading && nextWordNodes.length > 0 && currentNodeExists ? nextWordNodes.map((node, index) => ({ id: `next-edge-${index}`, source: currentNodeId, target: node.id, animated: false, - style: { stroke: '#cbd5e1', strokeWidth: 2, strokeDasharray: '5,5' }, + style: { stroke: '#cbd5e1', strokeWidth: 2, strokeDasharray: '5,5' }, type: 'smoothstep', })) : []; - setEdges([...pathEdges, ...nextEdges]); + // Create edges from current word to historical nodes (very faint) + const historicalEdges: Edge[] = ! isLoading && historicalNodes. length > 0 && currentNodeExists + ? historicalNodes. map((node, index) => ({ + id: `historical-edge-${index}`, + source: currentNodeId, + target: node.id, + animated: false, + style: { stroke: '#e5e7eb', strokeWidth: 1, strokeDasharray: '5,5' }, + type: 'smoothstep', + })) + : []; + + setEdges([...pathEdges, ...nextEdges, ...historicalEdges]); + + // Debug logging + console.log('Path Nodes:', pathNodes.map(n => n.id)); + console.log('Next Nodes:', nextWordNodes.map(n => n.id)); + console.log('Historical Nodes:', historicalNodes.map(n => n.id)); + console.log('Path Edges:', pathEdges); + console.log('Next Edges:', nextEdges); + console.log('Historical Edges:', historicalEdges); }, [path, currentWord, targetWord, words, isLoading, setNodes, setEdges]); - const handleNodeClick: NodeMouseHandler = (_event, node) => { + const handleNodeClick: NodeMouseHandler = (_event, node) => { // Handle path node clicks (revert) if (node.data.isPathNode && typeof node.data.pathIndex === 'number') { const index = node.data.pathIndex; @@ -212,8 +517,8 @@ export default function WordGraph({ } } - // Handle next word option clicks - if (node.id.startsWith('next-')) { + // Handle next word option clicks (including historical) + if (node.id. startsWith('next-') || node.id.startsWith('historical-')) { const word = node.data.label; onSelectWord(word); } @@ -229,7 +534,7 @@ export default function WordGraph({ }; const getThermometerLabel = () => { - if (proximity >= 80) return 'Very Hot! 🔥'; + if (proximity >= 80) return 'Very Hot! 🔥'; if (proximity >= 60) return 'Hot 🌡️'; if (proximity >= 40) return 'Warm ☀️'; if (proximity >= 20) return 'Cool ❄️'; @@ -246,6 +551,7 @@ export default function WordGraph({ onNodeClick={handleNodeClick} nodeTypes={nodeTypes} fitView + fitViewOptions={{ padding: 0.2 }} attributionPosition="bottom-right" minZoom={0.5} maxZoom={2} @@ -255,6 +561,7 @@ export default function WordGraph({ { if (node.id.startsWith('path-')) return '#a855f7'; + if (node.id.startsWith('historical-')) return '#9ca3af'; return '#cbd5e1'; }} maskColor="rgba(0, 0, 0, 0.1)" @@ -279,6 +586,18 @@ export default function WordGraph({
Visited
+
+
+ Old Options +
+
+
+ Path +
+
+
+ Options +
{/* Thermometer Panel */} @@ -310,6 +629,7 @@ export default function WordGraph({
  • • Click a word option to move forward
  • • Click a visited word to go back
  • +
  • • Greyed out words are old options
  • • Drag to pan, scroll to zoom
  • • Hover over words for definitions
@@ -326,4 +646,4 @@ export default function WordGraph({
); -} +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..0b31f6a --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from syn-city!") + + +if __name__ == "__main__": + main() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6ec260d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "syn-city", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a5d9b2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "syn-city" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "flask>=3.1.2", + "flask-cors>=6.0.2", + "redis>=7.1.0", +] diff --git a/server/state.py b/server/state.py new file mode 100644 index 0000000..5789e2a --- /dev/null +++ b/server/state.py @@ -0,0 +1 @@ +GAMES: dict = {} \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8333508 --- /dev/null +++ b/uv.lock @@ -0,0 +1,174 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "flask-cors" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + +[[package]] +name = "syn-city" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "flask-cors" }, + { name = "redis" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = ">=3.1.2" }, + { name = "flask-cors", specifier = ">=6.0.2" }, + { name = "redis", specifier = ">=7.1.0" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] From bac4d60ec8a371916157d5e05a8f07baea87ec85 Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sun, 18 Jan 2026 03:20:58 +0800 Subject: [PATCH 12/21] added similarity component --- server/routes/similarity.py | 38 +++++++++++++++++++ ...iltered words.json => filtered_words.json} | 0 server/services/game.py | 17 ++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 server/routes/similarity.py rename server/services/{filtered words.json => filtered_words.json} (100%) diff --git a/server/routes/similarity.py b/server/routes/similarity.py new file mode 100644 index 0000000..9c29652 --- /dev/null +++ b/server/routes/similarity.py @@ -0,0 +1,38 @@ +from flask import Blueprint, request, jsonify + +from sentence_transformers import SentenceTransformer +import numpy as np + +from services.game_manager import GameManager + +similarity_bp = Blueprint("similarity", __name__) + +# Load the model once at import time +model = SentenceTransformer("all-MiniLM-L6-v2") + + +@similarity_bp.route("/similarity", methods=["GET"]) +def similarity(): + game_id = request.headers.get("X-Game-Id") + current_word = request.headers.get("X-Current-Word") + + if not game_id or not current_word: + return jsonify({"error": "X-Game-Id and X-Current-Word headers required"}), 400 + + game = GameManager.load_game(game_id) + if not game: + return jsonify({"error": "invalid game id"}), 404 + + node = game.graph.nodes.get(current_word) + if not node: + return jsonify({"error": "invalid current word"}), 404 + + # Get embeddings + embeddings = model.encode([node.word, game.end.word]) + sim = np.dot(embeddings[0], embeddings[1]) / ( + np.linalg.norm(embeddings[0]) * np.linalg.norm(embeddings[1]) + ) + + return jsonify( + {"currentWord": node.word, "endWord": game.end.word, "similarity": sim} + ) diff --git a/server/services/filtered words.json b/server/services/filtered_words.json similarity index 100% rename from server/services/filtered words.json rename to server/services/filtered_words.json diff --git a/server/services/game.py b/server/services/game.py index 696990a..35e940c 100644 --- a/server/services/game.py +++ b/server/services/game.py @@ -2,7 +2,12 @@ import random from collections import deque -INPUT_FILE = "filtered words.json" +INPUT_FILE = "filtered_words.json" + +from sentence_transformers import SentenceTransformer +import numpy as np + +model = SentenceTransformer("all-MiniLM-L6-v2") class Node: @@ -117,6 +122,13 @@ def shortest_path(self, end: Node) -> int: return -1 + def similarity(self, node: Node) -> float: + embeddings = model.encode([node.word, self.end.word]) + sim = np.dot(embeddings[0], embeddings[1]) / ( + np.linalg.norm(embeddings[0]) * np.linalg.norm(embeddings[1]) + ) + return sim + def _play(self): # if start_word in self.graph.nodes: # start = self.graph.nodes[start_word] @@ -160,6 +172,9 @@ def _play(self): else: print("Invalid choice, try again.") + print(f"Closest distance to target: {self.shortest_path(curr)} steps") + print(f"Similarity to target: {self.similarity(curr):.4f}") + print( f"Congratulations! You reached the end word '{self.end.word}' in {num_actions} actions." ) From bf01ebe42ec0f3550ee9971fc866f5208b75beed Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sun, 18 Jan 2026 03:25:29 +0800 Subject: [PATCH 13/21] add python deps --- server/.python-version | 1 + server/pyproject.toml | 12 + server/uv.lock | 789 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 802 insertions(+) create mode 100644 server/.python-version create mode 100644 server/pyproject.toml create mode 100644 server/uv.lock diff --git a/server/.python-version b/server/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/server/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/server/pyproject.toml b/server/pyproject.toml new file mode 100644 index 0000000..27caaf6 --- /dev/null +++ b/server/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "syn-city" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "flask>=3.1.2", + "flask-cors>=6.0.2", + "redis>=7.1.0", + "sentence-transformers>=5.2.0", +] diff --git a/server/uv.lock b/server/uv.lock new file mode 100644 index 0000000..bec4948 --- /dev/null +++ b/server/uv.lock @@ -0,0 +1,789 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "flask-cors" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, + { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, + { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, + { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/a1/64e7b111e753307ffb7c5b6d039c52d4a91a47fa32a7f5bc377a49b22402/sentence_transformers-5.2.0.tar.gz", hash = "sha256:acaeb38717de689f3dab45d5e5a02ebe2f75960a4764ea35fea65f58a4d3019f", size = 381004, upload-time = "2025-12-11T14:12:31.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/d0/3b2897ef6a0c0c801e9fecca26bcc77081648e38e8c772885ebdd8d7d252/sentence_transformers-5.2.0-py3-none-any.whl", hash = "sha256:aa57180f053687d29b08206766ae7db549be5074f61849def7b17bf0b8025ca2", size = 493748, upload-time = "2025-12-11T14:12:29.516Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "syn-city" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "flask-cors" }, + { name = "redis" }, + { name = "sentence-transformers" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = ">=3.1.2" }, + { name = "flask-cors", specifier = ">=6.0.2" }, + { name = "redis", specifier = ">=7.1.0" }, + { name = "sentence-transformers", specifier = ">=5.2.0" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245, upload-time = "2025-11-12T15:22:39.027Z" }, + { url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804, upload-time = "2025-11-12T15:22:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132, upload-time = "2025-11-12T15:23:36.068Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845, upload-time = "2025-11-12T15:22:48.367Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558, upload-time = "2025-11-12T15:22:43.392Z" }, + { url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788, upload-time = "2025-11-12T15:23:52.109Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500, upload-time = "2025-11-12T15:24:08.788Z" }, + { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "triton" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488, upload-time = "2025-11-11T17:41:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192, upload-time = "2025-11-11T17:41:23.963Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] From 2a4ba04599fef6a0dcf26fa3acf660f387d5ae7e Mon Sep 17 00:00:00 2001 From: Kenneth Ong Date: Sun, 18 Jan 2026 03:47:29 +0800 Subject: [PATCH 14/21] linked frontend and backend --- client/src/App.tsx | 2 + client/src/components/GameScreen.tsx | 5 +- client/src/components/StartScreen.tsx | 69 ++++++++-------- client/src/hooks/useGame.ts | 113 +++++++++++--------------- client/vite.config.ts | 2 +- query | 1 + server/routes/dist.py | 2 +- server/services/game.py | 6 +- 8 files changed, 93 insertions(+), 107 deletions(-) create mode 100644 query diff --git a/client/src/App.tsx b/client/src/App.tsx index 80760c7..f92a466 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,6 +10,7 @@ interface GameConfig { startWord: string; targetWord: string; playerName: string; + gameId: string; } function App() { @@ -41,6 +42,7 @@ function App() { startWord={gameConfig.startWord} targetWord={gameConfig.targetWord} playerName={gameConfig.playerName} + gameId={gameConfig.gameId} onComplete={endGame} /> )} diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index c666bf3..a66c1a3 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -14,11 +14,12 @@ interface Props { startWord: string; targetWord: string; playerName: string; + gameId: string; onComplete: (result: GameResult) => void; } -export default function GameScreen({ startWord, targetWord, playerName, onComplete }: Props) { - const game = useGame(startWord, targetWord); +export default function GameScreen({ startWord, targetWord, playerName, gameId, onComplete }: Props) { + const game = useGame(startWord, targetWord, gameId); const timer = useTimer(!game.isComplete); useEffect(() => { diff --git a/client/src/components/StartScreen.tsx b/client/src/components/StartScreen.tsx index 72184f8..af9b24d 100644 --- a/client/src/components/StartScreen.tsx +++ b/client/src/components/StartScreen.tsx @@ -1,29 +1,43 @@ import { useState } from 'react'; interface Props { - onStart: (config: { startWord: string; targetWord: string; playerName: string }) => void; + onStart: (config: { startWord: string; targetWord: string; playerName: string; gameId: string }) => void; } export default function StartScreen({ onStart }: Props) { const [name, setName] = useState(''); + const [isLoading, setIsLoading] = useState(false); - // Mock puzzles (backend will provide these later) - const puzzles = [ - { id: 1, start: 'happy', end: 'sad', difficulty: 'easy' }, - { id: 2, start: 'big', end: 'small', difficulty: 'medium' }, - { id: 3, start: 'begin', end: 'finish', difficulty: 'hard' }, - ]; - - const handleStart = (puzzle: typeof puzzles[0]) => { + const handleStart = async () => { if (!name.trim()) { alert('Please enter your name!'); return; } - onStart({ - startWord: puzzle.start, - targetWord: puzzle.end, - playerName: name, - }); + + setIsLoading(true); + try { + const response = await fetch('/api/start', { + method: 'POST', + }); + + if (!response.ok) { + throw new Error('Failed to start game'); + } + + const data = await response.json(); + + onStart({ + startWord: data.startWord, + targetWord: data.targetWord, + playerName: name, + gameId: data.gameId, + }); + } catch (error) { + console.error('Error starting game:', error); + alert('Failed to start game. Please try again.'); + } finally { + setIsLoading(false); + } }; return ( @@ -44,26 +58,13 @@ export default function StartScreen({ onStart }: Props) { className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg mb-6 focus:outline-none focus:border-purple-500" /> -

Choose a puzzle:

- -
- {puzzles.map((puzzle) => ( - - ))} -
+
); diff --git a/client/src/hooks/useGame.ts b/client/src/hooks/useGame.ts index 8211a56..846d642 100644 --- a/client/src/hooks/useGame.ts +++ b/client/src/hooks/useGame.ts @@ -16,12 +16,7 @@ interface GameState { proximity: number; // 0-100, higher = closer to target } -interface DatamuseWord { - word: string; - defs?: string[]; -} - -export function useGame(startWord: string, targetWord: string) { +export function useGame(startWord: string, targetWord: string, gameId: string) { const [state, setState] = useState({ currentWord: startWord, targetWord, @@ -32,91 +27,75 @@ export function useGame(startWord: string, targetWord: string) { proximity: 0, }); - const calculateProximity = useCallback(async (word: string, target: string): Promise => { - const MAX_SYNONYMS_TO_CHECK = 50; - const DEFAULT_PROXIMITY_BASE = 50; - const PROXIMITY_PENALTY_PER_MOVE = 5; - + const calculateProximity = useCallback(async (word: string): Promise => { try { - // Check if the word is in the target's synonyms - const response = await fetch( - `https://api.datamuse.com/words?rel_syn=${target}&max=${MAX_SYNONYMS_TO_CHECK}` - ); - const data: DatamuseWord[] = await response.json(); - const synonyms = data.map((item) => item.word.toLowerCase()); - - if (word.toLowerCase() === target.toLowerCase()) { - return 100; - } - - if (synonyms.includes(word.toLowerCase())) { - return 80; + const response = await fetch('/api/dist', { + headers: { + 'X-Game-Id': gameId, + 'X-Current-Word': word, + }, + }); + + if (!response.ok) { + return 50; } + + const data = await response.json(); - // Check reverse - if target is in word's synonyms - const reverseResponse = await fetch( - `https://api.datamuse.com/words?rel_syn=${word}&max=${MAX_SYNONYMS_TO_CHECK}` - ); - const reverseData: DatamuseWord[] = await reverseResponse.json(); - const reverseSynonyms = reverseData.map((item) => item.word.toLowerCase()); - - if (reverseSynonyms.includes(target.toLowerCase())) { - return 70; + if (!data.reachable) { + return 0; } - - // Default based on path length - shorter path = better - return Math.max(0, DEFAULT_PROXIMITY_BASE - state.path.length * PROXIMITY_PENALTY_PER_MOVE); + + // Convert distance to proximity (lower distance = higher proximity) + // Distance of 0 = 100%, distance of 10+ = 0% + const maxDistance = 10; + const normalizedDistance = Math.min(data.distance || maxDistance, maxDistance); + return Math.round(100 - (normalizedDistance / maxDistance) * 100); } catch { return 50; } - }, [state.path.length]); + }, [gameId]); const fetchWords = useCallback(async (word: string) => { setState(prev => ({ ...prev, isLoading: true })); try { - // Fetch synonyms, antonyms, and related words in parallel - const [synonymResponse, antonymResponse, relatedResponse] = await Promise.all([ - fetch(`https://api.datamuse.com/words?rel_syn=${word}&max=4&md=d`), - fetch(`https://api.datamuse.com/words?rel_ant=${word}&max=2&md=d`), - fetch(`https://api.datamuse.com/words?rel_trg=${word}&max=2&md=d`), - ]); - - const [synonymData, antonymData, relatedData]: [DatamuseWord[], DatamuseWord[], DatamuseWord[]] = await Promise.all([ - synonymResponse.json(), - antonymResponse.json(), - relatedResponse.json(), - ]); + const response = await fetch('/api/next', { + headers: { + 'X-Game-Id': gameId, + 'X-Current-Word': word, + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch words'); + } + + const data = await response.json(); - // Map each type with metadata - const synonyms: WordWithMetadata[] = synonymData.map((item) => ({ - word: item.word, - definition: item.defs && item.defs.length > 0 - ? item.defs[0].replace(/^\w+\t/, '') - : 'No definition available', + // Map backend response to WordWithMetadata format + const synonyms: WordWithMetadata[] = (data.synonyms || []).map((word: string) => ({ + word, + definition: 'Synonym', type: 'synonym' as const, })); - const antonyms: WordWithMetadata[] = antonymData.map((item) => ({ - word: item.word, - definition: item.defs && item.defs.length > 0 - ? item.defs[0].replace(/^\w+\t/, '') - : 'No definition available', + const antonyms: WordWithMetadata[] = (data.antonyms || []).map((word: string) => ({ + word, + definition: 'Antonym', type: 'antonym' as const, })); - const related: WordWithMetadata[] = relatedData.map((item) => ({ - word: item.word, - definition: item.defs && item.defs.length > 0 - ? item.defs[0].replace(/^\w+\t/, '') - : 'No definition available', + const related: WordWithMetadata[] = (data.related || []).map((word: string) => ({ + word, + definition: 'Related word', type: 'related' as const, })); const allWords = [...synonyms, ...antonyms, ...related]; // Calculate proximity to target - const proximity = await calculateProximity(word, state.targetWord); + const proximity = await calculateProximity(word); setState(prev => ({ ...prev, @@ -134,7 +113,7 @@ export function useGame(startWord: string, targetWord: string) { isLoading: false })); } - }, [state.targetWord, calculateProximity]); + }, [gameId, calculateProximity]); const selectWord = useCallback((word: string) => { if (word === 'No words found' || word === 'Error loading words') { diff --git a/client/vite.config.ts b/client/vite.config.ts index a51cfe1..90dbc4a 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ server: { proxy: { '/api': { - target: 'http://localhost:3000', + target: 'http://localhost:3001', changeOrigin: true, secure: false, ws: true, diff --git a/query b/query new file mode 100644 index 0000000..0ad3568 --- /dev/null +++ b/query @@ -0,0 +1 @@ +Redis diff --git a/server/routes/dist.py b/server/routes/dist.py index a80ad47..53fdf04 100644 --- a/server/routes/dist.py +++ b/server/routes/dist.py @@ -17,7 +17,7 @@ def distance(): if not game: return jsonify({"error": "invalid game id"}), 404 - target = game.end + target = game.end.word dist = game.shortest_path(game.graph.nodes[current_word]) return jsonify( diff --git a/server/services/game.py b/server/services/game.py index 696990a..8f4bc05 100644 --- a/server/services/game.py +++ b/server/services/game.py @@ -1,8 +1,9 @@ import json import random from collections import deque +from pathlib import Path -INPUT_FILE = "filtered words.json" +INPUT_FILE = Path(__file__).parent / "filtered words.json" class Node: @@ -24,7 +25,8 @@ def to_dict(self): class Graph: def __init__(self): self.nodes = {} - compressed_graph = json.load(open(INPUT_FILE, "r", encoding="utf-8")) + with open(INPUT_FILE, "r", encoding="utf-8") as f: + compressed_graph = json.load(f) for word, relations in compressed_graph.items(): for relation, targets in relations.items(): for target in targets: From b58b60369aa004bb99e13426d9a51d332587ad30 Mon Sep 17 00:00:00 2001 From: Asher Date: Sun, 18 Jan 2026 05:22:49 +0800 Subject: [PATCH 15/21] Change colour scheme to maroon and white and fix name --- client/package-lock.json | 1339 +++++++++++++++++++------ client/package.json | 7 +- client/postcss.config.js | 6 + client/src/App.tsx | 4 +- client/src/components/EndScreen.tsx | 28 +- client/src/components/GameScreen.tsx | 28 +- client/src/components/StartScreen.tsx | 14 +- client/src/components/WordGraph.tsx | 272 ++--- client/src/index.css | 4 +- client/tailwind.config.js | 27 + client/vite.config.ts | 5 +- 11 files changed, 1189 insertions(+), 545 deletions(-) create mode 100644 client/postcss.config.js create mode 100644 client/tailwind.config.js diff --git a/client/package-lock.json b/client/package-lock.json index c745982..696698a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,11 +8,9 @@ "name": "client", "version": "0.0.0", "dependencies": { - "@tailwindcss/vite": "^4.1.18", "react": "^19.2.0", "react-dom": "^19.2.0", - "reactflow": "^11.11.4", - "tailwindcss": "^4.1.18" + "reactflow": "^11.11.4" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -20,15 +18,31 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -318,6 +332,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -334,6 +349,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -350,6 +366,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -366,6 +383,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -382,6 +400,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -398,6 +417,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -414,6 +434,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -430,6 +451,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -446,6 +468,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -462,6 +485,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -478,6 +502,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -494,6 +519,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -510,6 +536,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -526,6 +553,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -542,6 +570,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -558,6 +587,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -574,6 +604,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -590,6 +621,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -606,6 +638,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -622,6 +655,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,6 +672,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -654,6 +689,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -670,6 +706,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -686,6 +723,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -702,6 +740,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -718,6 +757,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -940,6 +980,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -950,6 +991,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -960,6 +1002,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -969,18 +1012,58 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@reactflow/background": { "version": "11.3.14", "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", @@ -1097,6 +1180,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1110,6 +1194,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1123,6 +1208,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1136,6 +1222,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1149,6 +1236,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1162,6 +1250,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1175,6 +1264,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1188,6 +1278,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1201,6 +1292,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1214,6 +1306,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1227,6 +1320,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1240,6 +1334,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1253,6 +1348,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1266,6 +1362,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1279,6 +1376,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1292,6 +1390,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1305,6 +1404,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1318,6 +1418,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1331,6 +1432,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1344,6 +1446,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1357,6 +1460,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1370,6 +1474,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1383,6 +1488,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1396,6 +1502,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1409,269 +1516,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, - "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", - "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "tailwindcss": "4.1.18" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1974,6 +1825,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/geojson": { @@ -1993,7 +1845,7 @@ "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2365,28 +2217,119 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true, "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.15", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", - "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/brace-expansion": { @@ -2400,6 +2343,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2444,6 +2400,16 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001764", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", @@ -2482,6 +2448,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/classcat": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", @@ -2508,6 +2512,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2537,6 +2551,19 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2678,11 +2705,28 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=8" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -2690,23 +2734,11 @@ "dev": true, "license": "ISC" }, - "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2958,6 +2990,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2972,10 +3034,21 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -3002,6 +3075,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3040,10 +3126,25 @@ "dev": true, "license": "ISC" }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3054,6 +3155,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3090,12 +3201,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3106,6 +3211,19 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3160,6 +3278,35 @@ "node": ">=0.8.19" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3183,6 +3330,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3194,7 +3351,10 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3294,7 +3454,10 @@ "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, "license": "MPL-2.0", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -3326,11 +3489,13 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3346,11 +3511,13 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3366,11 +3533,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3386,11 +3555,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3406,11 +3577,13 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3426,11 +3599,13 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3446,11 +3621,13 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3466,11 +3643,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3486,11 +3665,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3506,11 +3687,13 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3526,11 +3709,13 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3539,6 +3724,23 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3572,23 +3774,51 @@ "yallist": "^3.0.2" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "engines": { + "node": ">= 8" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { "node": "*" @@ -3601,10 +3831,23 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -3633,6 +3876,36 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3716,16 +3989,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3734,10 +4016,31 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -3762,6 +4065,146 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3782,6 +4225,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -3831,6 +4295,63 @@ "react-dom": ">=17" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3841,10 +4362,22 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3885,6 +4418,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3928,6 +4485,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3946,6 +4504,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3959,29 +4540,95 @@ "node": ">=8" } }, - "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" } }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3994,6 +4641,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -4007,6 +4667,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4062,7 +4729,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -4115,10 +4782,18 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -4222,6 +4897,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/client/package.json b/client/package.json index bf63d19..c51c69f 100644 --- a/client/package.json +++ b/client/package.json @@ -10,11 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "@tailwindcss/vite": "^4.1.18", "react": "^19.2.0", "react-dom": "^19.2.0", - "reactflow": "^11.11.4", - "tailwindcss": "^4.1.18" + "reactflow": "^11.11.4" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -22,10 +20,13 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.1", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" diff --git a/client/postcss.config.js b/client/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/client/src/App.tsx b/client/src/App.tsx index f92a466..9a1ac7d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -35,7 +35,7 @@ function App() { }; return ( -
+
{screen === 'start' && } {screen === 'game' && gameConfig && ( +
🎉
-

Congratulations!

-

{result.playerName}

-

You reached the target word!

+

Congratulations!

+

{result.playerName}

+

You reached the target word!

-
+
-

Time

-

+

Time

+

{minutes}:{seconds.toString().padStart(2, '0')}

-

Moves

-

{result.moves}

+

Moves

+

{result.moves}

-

Your path:

+

Your path:

{result.path.map((word, index) => (
- + {word} {index < result.path.length - 1 && ( - + )}
))} @@ -53,11 +53,11 @@ export default function EndScreen({ result, onRestart }: Props) {
); -} +} \ No newline at end of file diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index a66c1a3..c119d65 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -24,7 +24,7 @@ export default function GameScreen({ startWord, targetWord, playerName, gameId, useEffect(() => { game.fetchWords(startWord); - // eslint-disable-next-line react-hooks/exhaustive-deps -- Only run on mount to initialize game + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -36,35 +36,35 @@ export default function GameScreen({ startWord, targetWord, playerName, gameId, timeSeconds: timer.seconds, }); } - // eslint-disable-next-line react-hooks/exhaustive-deps -- Only trigger on completion status change + // eslint-disable-next-line react-hooks/exhaustive-deps }, [game.isComplete]); return ( -
+
{/* Header */} -
+
-

Player

-

{playerName}

+

Player

+

{playerName}

-

Target

-

{targetWord}

+

Target

+

{targetWord}

-

Time

-

{timer.formattedTime}

+

Time

+

{timer.formattedTime}

-

Moves

-

{game.path.length - 1}

+

Moves

+

{game.path.length - 1}

{/* Main Game Area with Interactive Graph */} -
+
); -} +} \ No newline at end of file diff --git a/client/src/components/StartScreen.tsx b/client/src/components/StartScreen.tsx index af9b24d..e4ebcef 100644 --- a/client/src/components/StartScreen.tsx +++ b/client/src/components/StartScreen.tsx @@ -41,12 +41,12 @@ export default function StartScreen({ onStart }: Props) { }; return ( -
+
-

- 🎯 Synonym Sprint +

+ SYNCITY

-

+

Race through synonyms to reach the target word!

@@ -55,17 +55,17 @@ export default function StartScreen({ onStart }: Props) { placeholder="Enter your name" value={name} onChange={(e) => setName(e.target.value)} - className="w-full px-4 py-3 border-2 border-gray-300 rounded-lg mb-6 focus:outline-none focus:border-purple-500" + className="w-full px-4 py-3 border-2 border-maroon-200 rounded-lg mb-6 focus:outline-none focus:ring-2 focus:ring-maroon-800 focus:border-transparent" />
); -} +} \ No newline at end of file diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index 5d50d2b..b955e8e 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -25,8 +25,8 @@ interface Props { interface HistoricalNode { word: string; position: { x: number; y: number }; - definition?: string; - type?: string; + definition?: string; + type?: string; } // Custom node component with tooltip @@ -36,9 +36,9 @@ function CustomNode({ data }: { backgroundColor: string; color: string; borderColor: string; - definition?: string; - pathIndex?: number; - isPathNode?: boolean; + definition?: string; + pathIndex?: number; + isPathNode?: boolean; isHistorical?: boolean; } }) { @@ -59,12 +59,12 @@ function CustomNode({ data }: { opacity: data.isHistorical ? 0.5 : 1, }} > - {data. label} + {data.label}
{showTooltip && data.definition && ( -
+
{data.definition} -
+
)}
@@ -72,7 +72,7 @@ function CustomNode({ data }: { } const nodeTypes = { - custom: CustomNode, + custom: CustomNode, }; export default function WordGraph({ @@ -88,17 +88,14 @@ export default function WordGraph({ const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); - // Store positions of all words to preserve them when selected - const wordPositionsRef = useRef>(new Map()); - - // Store all historical option nodes that have been shown + const wordPositionsRef = useRef>(new Map()); const historicalNodesRef = useRef>(new Map()); useEffect(() => { - // Collision detection helper - const MIN_DISTANCE = 120; // Minimum distance between nodes to prevent overlap + // [Keeping all the existing logic for node positioning - just updating colors] + const MIN_DISTANCE = 120; - const checkCollision = (pos: { x: number; y: number }, existingPositions: Array<{ x: number; y: number }>) => { + const checkCollision = (pos: { x: number; y: number }, existingPositions: Array<{ x: number; y: number }>) => { for (const existing of existingPositions) { const distance = Math.sqrt( Math.pow(pos.x - existing.x, 2) + Math.pow(pos.y - existing.y, 2) @@ -110,36 +107,27 @@ export default function WordGraph({ return false; }; - // Adjust position to avoid collisions const findNonCollidingPosition = ( - basePos: { x: number; y: number }, + basePos: { x: number; y: number }, centerX: number, - centerY: number, + centerY: number, existingPositions: Array<{ x: number; y: number }>, - baseAngle?: number + baseAngle?: number ) => { - let position = { ... basePos }; + let position = { ...basePos }; let attempts = 0; const maxAttempts = 30; while (checkCollision(position, existingPositions) && attempts < maxAttempts) { attempts++; - - // Calculate current distance from center const distance = Math.sqrt( Math.pow(position.x - centerX, 2) + Math.pow(position.y - centerY, 2) ); - - // If baseAngle is provided, use it; otherwise calculate from current position let angle = baseAngle; if (angle === undefined) { - angle = Math.atan2(position. y - centerY, position.x - centerX) * (180 / Math.PI); + angle = Math.atan2(position.y - centerY, position.x - centerX) * (180 / Math.PI); } - - // Try increasing radius - const newDistance = distance + 30; // Increase by 30px each attempt - - // Add slight angle variation to spread out + const newDistance = distance + 30; const angleVariation = (attempts % 2 === 0 ? 1 : -1) * (attempts * 5); const adjustedAngle = angle + angleVariation; const radian = (adjustedAngle * Math.PI) / 180; @@ -153,39 +141,36 @@ export default function WordGraph({ return position; }; - // Create nodes from path - const pathNodes: Node[] = path.map((word, index) => { + // Create nodes from path with maroon colors + const pathNodes: Node[] = path.map((word, index) => { const isStart = index === 0; const isCurrent = word === currentWord; - const isTarget = word. toLowerCase() === targetWord.toLowerCase(); + const isTarget = word.toLowerCase() === targetWord.toLowerCase(); - let backgroundColor = '#e5e7eb'; // gray for visited - let color = '#374151'; - let borderColor = '#d1d5db'; + let backgroundColor = '#f9d0d9'; // maroon-200 for visited + let color = '#791f3e'; + let borderColor = '#f4a8b8'; if (isTarget) { backgroundColor = '#10b981'; // green for target color = 'white'; borderColor = '#059669'; } else if (isCurrent) { - backgroundColor = '#a855f7'; // purple for current + backgroundColor = '#791f3e'; // maroon-900 for current color = 'white'; - borderColor = '#9333ea'; + borderColor = '#430b1e'; } else if (isStart) { - backgroundColor = '#6366f1'; // indigo for start + backgroundColor: '#8e2043'; // maroon-800 for start color = 'white'; - borderColor = '#4f46e5'; + borderColor = '#791f3e'; } - // Check if this word has a saved position (was previously a next word option) const savedPosition = wordPositionsRef.current.get(word.toLowerCase()); let position; if (savedPosition) { - // Use the saved position from when it was a next word option position = savedPosition; } else { - // Default linear positioning for the start word or words without saved positions position = { x: index * 200, y: 100 }; wordPositionsRef.current.set(word.toLowerCase(), position); } @@ -198,7 +183,7 @@ export default function WordGraph({ backgroundColor, color, borderColor, - definition: `Click to revert to this word (${isStart ? 'start' : isTarget ? 'target' : isCurrent ? 'current' : 'visited'})`, + definition: `Click to revert to this word (${isStart ? 'start' : isTarget ? 'target' : isCurrent ? 'current' : 'visited'})`, pathIndex: index, isPathNode: true, }, @@ -207,79 +192,67 @@ export default function WordGraph({ }; }); - // Group words by type const synonyms = words.filter(w => w.type === 'synonym'); - const antonyms = words. filter(w => w.type === 'antonym'); + const antonyms = words.filter(w => w.type === 'antonym'); const related = words.filter(w => w.type === 'related'); - const other = words.filter(w => ! w.type || (w.type !== 'synonym' && w.type !== 'antonym' && w.type !== 'related')); + const other = words.filter(w => !w.type || (w.type !== 'synonym' && w.type !== 'antonym' && w.type !== 'related')); - // Create next word options nodes with circular positioning const nextWordNodes: Node[] = []; const currentWordSet = new Set(words.map(w => w.word.toLowerCase())); - if (! isLoading && words.length > 0 && + if (!isLoading && words.length > 0 && words[0].word !== 'No words found' && words[0].word !== 'Error loading words') { - // Get existing positions from path nodes const existingPositions = pathNodes.map(node => node.position); - - // Get the current word's position const currentNodePosition = wordPositionsRef.current.get(currentWord.toLowerCase()) || - { x: (path. length - 1) * 200, y: 100 }; + { x: (path.length - 1) * 200, y: 100 }; const currentX = currentNodePosition.x; const currentY = currentNodePosition.y; - const baseRadius = 250; // Increased base distance from current node - const radiusIncrement = 90; // Space between layers + const baseRadius = 250; + const radiusIncrement = 90; - // Helper function to calculate circular position const getCircularPosition = (angle: number, distance: number) => { - const radian = (angle * Math. PI) / 180; + const radian = (angle * Math.PI) / 180; return { x: currentX + Math.cos(radian) * distance, y: currentY + Math.sin(radian) * distance, }; }; - // Helper function to distribute nodes in an angular range with proper spacing const distributeNodesInSection = ( nodeCount: number, startAngle: number, angleRange: number, baseRadius: number ) => { - const positions: Array<{ x: number; y: number; layer: number; angle: number }> = []; - const maxNodesPerLayer = 4; // Maximum nodes per radius layer to avoid overlap + const positions: Array<{ x: number; y: number; layer: number; angle: number }> = []; + const maxNodesPerLayer = 4; for (let i = 0; i < nodeCount; i++) { const layer = Math.floor(i / maxNodesPerLayer); const indexInLayer = i % maxNodesPerLayer; const nodesInThisLayer = Math.min(nodeCount - layer * maxNodesPerLayer, maxNodesPerLayer); - // Calculate angle for this node within its layer const angleStep = angleRange / Math.max(nodesInThisLayer, 1); const angle = startAngle + (indexInLayer * angleStep) + (angleStep / 2) - (angleRange / 2); - - // Calculate distance with layer offset const distance = baseRadius + (layer * radiusIncrement); const position = getCircularPosition(angle, distance); - positions.push({ ... position, layer, angle }); + positions.push({ ...position, layer, angle }); } return positions; }; - // Track all new node positions to check for collisions - const allNewPositions: Array<{ x: number; y: number }> = []; + const allNewPositions: Array<{ x: number; y: number }> = []; - // Helper function to create node const createWordNode = ( wordData: WordWithMetadata, index: number, - basePosition: { x: number; y: number; angle: number }, + basePosition: { x: number; y: number; angle: number }, currentX: number, - currentY: number + currentY: number ) => { const { word, definition, type } = wordData; const isTarget = word.toLowerCase() === targetWord.toLowerCase(); @@ -310,18 +283,16 @@ export default function WordGraph({ basePosition, currentX, currentY, - [... existingPositions, ...allNewPositions], + [...existingPositions, ...allNewPositions], basePosition.angle ); allNewPositions.push(position); - // Save this position for future reference - if (! wordPositionsRef.current.has(word.toLowerCase())) { - wordPositionsRef.current. set(word.toLowerCase(), { x: position.x, y: position.y }); + if (!wordPositionsRef.current.has(word.toLowerCase())) { + wordPositionsRef.current.set(word.toLowerCase(), { x: position.x, y: position.y }); } - // Add to historical nodes historicalNodesRef.current.set(word.toLowerCase(), { word, position: { x: position.x, y: position.y }, @@ -333,116 +304,80 @@ export default function WordGraph({ id: `next-${word}-${nextWordNodes.length}`, type: 'custom', data: { - label: word, + label: word, backgroundColor, color, borderColor, definition: definition || type || 'Word option', }, - position: { x: position.x, y: position.y }, + position: { x: position.x, y: position.y }, draggable: false, }; }; - // Synonyms spawn in top section (-90° centered, 100° range) - const synonymPositions = distributeNodesInSection( - synonyms.length, - -90, - 100, - baseRadius - ); - + const synonymPositions = distributeNodesInSection(synonyms.length, -90, 100, baseRadius); synonyms.forEach((wordData, index) => { const node = createWordNode(wordData, index, synonymPositions[index], currentX, currentY); nextWordNodes.push(node); }); - // Antonyms spawn in bottom-right section (60° centered, 100° range) - const antonymPositions = distributeNodesInSection( - antonyms.length, - 60, - 100, - baseRadius - ); - - antonyms. forEach((wordData, index) => { + const antonymPositions = distributeNodesInSection(antonyms.length, 60, 100, baseRadius); + antonyms.forEach((wordData, index) => { const node = createWordNode(wordData, index, antonymPositions[index], currentX, currentY); nextWordNodes.push(node); }); - // Related words spawn in left section (180° centered, 100° range) - const relatedPositions = distributeNodesInSection( - related.length, - 180, - 100, - baseRadius - ); - + const relatedPositions = distributeNodesInSection(related.length, 180, 100, baseRadius); related.forEach((wordData, index) => { const node = createWordNode(wordData, index, relatedPositions[index], currentX, currentY); nextWordNodes.push(node); }); - // Other words - const otherPositions = distributeNodesInSection( - other.length, - -135, - 80, - baseRadius - ); - + const otherPositions = distributeNodesInSection(other.length, -135, 80, baseRadius); other.forEach((wordData, index) => { const node = createWordNode(wordData, index, otherPositions[index], currentX, currentY); - nextWordNodes. push(node); + nextWordNodes.push(node); }); } - // Collect all occupied positions (path + current options) - const allOccupiedPositions: Array<{ x: number; y: number }> = [ + const allOccupiedPositions: Array<{ x: number; y: number }> = [ ...pathNodes.map(node => node.position), ...nextWordNodes.map(node => node.position), ]; - // Add historical nodes that are not in current path or current options const pathWordSet = new Set(path.map(w => w.toLowerCase())); - const historicalNodes: Node[] = []; + const historicalNodes: Node[] = []; - // Get the current word's position for calculating adjustments const currentNodePosition = wordPositionsRef.current.get(currentWord.toLowerCase()) || { x: (path.length - 1) * 200, y: 100 }; historicalNodesRef.current.forEach((historicalNode, wordKey) => { - // Skip if this word is in the current path or current word options - if (! pathWordSet.has(wordKey) && !currentWordSet.has(wordKey)) { - // Check if the historical node's stored position would collide + if (!pathWordSet.has(wordKey) && !currentWordSet.has(wordKey)) { let finalPosition = historicalNode.position; if (checkCollision(historicalNode.position, allOccupiedPositions)) { - // Find a non-colliding position near the original finalPosition = findNonCollidingPosition( - historicalNode. position, - currentNodePosition. x, - currentNodePosition. y, + historicalNode.position, + currentNodePosition.x, + currentNodePosition.y, allOccupiedPositions, - undefined // Let it calculate angle from position + undefined ); - // Update the stored position historicalNodesRef.current.set(wordKey, { ...historicalNode, position: finalPosition, }); } - // Add to occupied positions so subsequent historical nodes avoid it allOccupiedPositions.push(finalPosition); historicalNodes.push({ id: `historical-${historicalNode.word}`, type: 'custom', data: { - label: historicalNode. word, - backgroundColor: '#9ca3af', // gray + label: historicalNode.word, + backgroundColor: '#9ca3af', color: '#374151', borderColor: '#6b7280', definition: historicalNode.definition || 'Previously shown option', @@ -456,59 +391,47 @@ export default function WordGraph({ setNodes([...pathNodes, ...nextWordNodes, ...historicalNodes]); - // Create edges connecting all selected path nodes - const pathEdges: Edge[] = []; + const pathEdges: Edge[] = []; for (let i = 0; i < path.length - 1; i++) { pathEdges.push({ id: `path-edge-${i}`, source: `path-${path[i]}-${i}`, target: `path-${path[i + 1]}-${i + 1}`, - animated: i === path.length - 2, // Animate the most recent connection - style: { stroke: '#8b5cf6', strokeWidth: 3 }, + animated: i === path.length - 2, + style: { stroke: '#791f3e', strokeWidth: 3 }, type: 'smoothstep', }); } - // Create edges from current word to all next word options (not historical) const currentNodeId = `path-${currentWord}-${path.length - 1}`; const currentNodeExists = pathNodes.some(node => node.id === currentNodeId); - const nextEdges: Edge[] = ! isLoading && nextWordNodes.length > 0 && currentNodeExists + const nextEdges: Edge[] = !isLoading && nextWordNodes.length > 0 && currentNodeExists ? nextWordNodes.map((node, index) => ({ id: `next-edge-${index}`, source: currentNodeId, target: node.id, animated: false, - style: { stroke: '#cbd5e1', strokeWidth: 2, strokeDasharray: '5,5' }, + style: { stroke: '#cbd5e1', strokeWidth: 2, strokeDasharray: '5,5' }, type: 'smoothstep', })) : []; - // Create edges from current word to historical nodes (very faint) - const historicalEdges: Edge[] = ! isLoading && historicalNodes. length > 0 && currentNodeExists - ? historicalNodes. map((node, index) => ({ + const historicalEdges: Edge[] = !isLoading && historicalNodes.length > 0 && currentNodeExists + ? historicalNodes.map((node, index) => ({ id: `historical-edge-${index}`, source: currentNodeId, target: node.id, animated: false, - style: { stroke: '#e5e7eb', strokeWidth: 1, strokeDasharray: '5,5' }, + style: { stroke: '#e5e7eb', strokeWidth: 1, strokeDasharray: '5,5' }, type: 'smoothstep', })) : []; setEdges([...pathEdges, ...nextEdges, ...historicalEdges]); - - // Debug logging - console.log('Path Nodes:', pathNodes.map(n => n.id)); - console.log('Next Nodes:', nextWordNodes.map(n => n.id)); - console.log('Historical Nodes:', historicalNodes.map(n => n.id)); - console.log('Path Edges:', pathEdges); - console.log('Next Edges:', nextEdges); - console.log('Historical Edges:', historicalEdges); }, [path, currentWord, targetWord, words, isLoading, setNodes, setEdges]); - const handleNodeClick: NodeMouseHandler = (_event, node) => { - // Handle path node clicks (revert) + const handleNodeClick: NodeMouseHandler = (_event, node) => { if (node.data.isPathNode && typeof node.data.pathIndex === 'number') { const index = node.data.pathIndex; const word = path[index]; @@ -517,24 +440,22 @@ export default function WordGraph({ } } - // Handle next word option clicks (including historical) - if (node.id. startsWith('next-') || node.id.startsWith('historical-')) { + if (node.id.startsWith('next-') || node.id.startsWith('historical-')) { const word = node.data.label; onSelectWord(word); } }; - // Thermometer colors based on proximity const getThermometerColor = () => { - if (proximity >= 80) return '#ef4444'; // hot red - if (proximity >= 60) return '#f97316'; // orange - if (proximity >= 40) return '#eab308'; // yellow - if (proximity >= 20) return '#3b82f6'; // blue - return '#06b6d4'; // cold cyan + if (proximity >= 80) return '#ef4444'; + if (proximity >= 60) return '#f97316'; + if (proximity >= 40) return '#eab308'; + if (proximity >= 20) return '#3b82f6'; + return '#06b6d4'; }; const getThermometerLabel = () => { - if (proximity >= 80) return 'Very Hot! 🔥'; + if (proximity >= 80) return 'Very Hot! 🔥'; if (proximity >= 60) return 'Hot 🌡️'; if (proximity >= 40) return 'Warm ☀️'; if (proximity >= 20) return 'Cool ❄️'; @@ -560,16 +481,15 @@ export default function WordGraph({ { - if (node.id.startsWith('path-')) return '#a855f7'; + if (node.id.startsWith('path-')) return '#791f3e'; if (node.id.startsWith('historical-')) return '#9ca3af'; return '#cbd5e1'; }} maskColor="rgba(0, 0, 0, 0.1)" /> - {/* Legend Panel */} - -

Legend

+ +

Legend

Synonym @@ -583,15 +503,15 @@ export default function WordGraph({ Related
-
+
Visited
Old Options
-
-
+
+
Path
@@ -600,17 +520,16 @@ export default function WordGraph({
- {/* Thermometer Panel */} - +
-

Proximity

+

Proximity

{getThermometerLabel()}
-
+
-
{proximity}%
+
{proximity}%
- {/* Instructions Panel */} - -

How to play:

-
    + +

    How to play:

    +
    • • Click a word option to move forward
    • • Click a visited word to go back
    • • Greyed out words are old options
    • @@ -636,10 +554,10 @@ export default function WordGraph({ {isLoading && ( - +
      -
      - Loading words... +
      + Loading words...
      )} diff --git a/client/src/index.css b/client/src/index.css index 73a943c..bd6213e 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1 +1,3 @@ -@import "tailwindcss" +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/client/tailwind.config.js b/client/tailwind.config.js new file mode 100644 index 0000000..0d08665 --- /dev/null +++ b/client/tailwind.config.js @@ -0,0 +1,27 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + maroon: { + 50: '#fdf2f4', + 100: '#fce7eb', + 200: '#f9d0d9', + 300: '#f4a8b8', + 400: '#ec7892', + 500: '#e0516f', + 600: '#cc315a', + 700: '#ab234a', + 800: '#8e2043', + 900: '#791f3e', + 950: '#430b1e', + }, + }, + }, + }, + plugins: [], +} \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts index 90dbc4a..1d19596 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,10 +1,9 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react(), tailwindcss()], + plugins: [react()], server: { proxy: { '/api': { @@ -15,4 +14,4 @@ export default defineConfig({ }, }, } -}) +}) \ No newline at end of file From af5c173efb887f6feb560fcfd52dfb45161fa01e Mon Sep 17 00:00:00 2001 From: Asher Date: Sun, 18 Jan 2026 06:18:00 +0800 Subject: [PATCH 16/21] Enhance UI --- ...vector-illustration-png-image_14724142.png | Bin 0 -> 18900 bytes client/src/components/EndScreen.tsx | 65 ++++++++++++------ client/src/components/StartScreen.tsx | 37 ++++++++-- client/src/components/WordGraph.tsx | 6 +- 4 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 client/pngtree-network-and-connectivity-icon-abstract-node-link-structure-outline-vector-illustration-png-image_14724142.png diff --git a/client/pngtree-network-and-connectivity-icon-abstract-node-link-structure-outline-vector-illustration-png-image_14724142.png b/client/pngtree-network-and-connectivity-icon-abstract-node-link-structure-outline-vector-illustration-png-image_14724142.png new file mode 100644 index 0000000000000000000000000000000000000000..90b02cabaf68524a14e0c9400a93fac84a444fa1 GIT binary patch literal 18900 zcmbSSV|OO75^lG)ZQJJ7?OWTnZFg(i++yo)ZQHiq+O~W5-k)$kBsrN(a3+&;CVBEi zD#}YDz~aJ!fPf$Xq{Ng#KtL`3o1nq}NoFs~_5T&%7Q%AEARu+|a34kx|LVjhQp$25 zAl{T9Ab}wuAaDOffhQm!Zp>`Y0uU2c^<2NygUUc3THc}{U`=9+G8Pg?76(r;{DA=hUj|)x zmo1KhAuNodxwp0QI)(Q#BAkTT~0yHP3>9MLt*COLYV{j zzU^}%JLCF%s&o9u<^FDxb%MU(|IeyW>l4C#RR{98vBL`)o>LNQYbb%9eAnwEb}@??HU~PI{}q`O@)H%99H-#c7_?ix`mt(^oLYs6j80I~9GvS_)S!Bx z3bg7^B!8y~{MsRJZQo0Q;&Nb?4TWH?d>s2Bux!@coOhc-s-LMN|0y zj=f+fl!G}~KW{O(#p8p5vi7^v}{KpaS51amnEu zwj)}j+LlwSbnhwq{&>iL;l@OIjH$!5=0Mi-)Z56I!~LZJdD;I2i9+6i47ShC$P?p5 z_%gPEghn8C>g<7ni|VW2$<@Ehq9fqYoA5amSM?gfWXzTf0%f7svon)ke6l+ z#tGMr8j>HfzvBt$H;M*@!yfBIm1CbSsN_Bc)afr{SqbqO0y?)-Z2LX7>pj!nf9^Ip z(Doy)*F^L9$53^vAO|u->^xM{^!2wbxH;r{h6qIiE5Z>DA}#)1CCI*}g<(|9^^vEx z(TuDS0OI8u+YDWLY8*ae;&(7uMvEb1xB8bImA<4!N;u!UAec-Olo5}7A9@LARuh76 z2wSXByVmY69-?=Xta;=KQ>Y^}XgV~O7v11vT>fACigdy_$Uf9rMj(A%y~3f5OyBQH zu9LZ0QvryVW(brGlhPv)wh<^V;te_Z`@gW83RdRC5MA%;aPkd#YlvrwM1?LR>2A^e zXAjJb_t?oaUFZXret%a_+*O!_JvVf_wQzS=CC2RM?aY;wY8BgRLU} zxh{(ot_(UgCQ0Rt$7P-N0rBQR=4z-8ve8LJX;sw*!i3`ISXQy0&kY})47r!1|I?`8 z{FZiJ^sXKA@(T|wV=)M{No%{W6OVD23zl1}e&1Ey3iNrVY4BOFw-YqJFqMy;p=AC` zvk>B?d(i6v3(UHKzqgHlZHEhESSN(Z5yrYShA}^yfw1p`E3e@CHtkn=ywQsm84=U2 zl&IjOG(?^svs^UCcUZQvBzCLcm%Q(h4gDY!__Z-qM<;fXBQqIpUh)CGEqCr}$D86u z{E{J_V)h~YQC6#N5&ZX_K7Ix-s9ZiIA!fdI?DSyZvk;eVSjR2xTskQU)^em=*ROA& z2B-kD0nFRbXn3S=#-R{V&s9%s( z`td~Ui=_qi*%Of(E`-+NFK)ak+&@UBT|GdXOxTC0_8OwKK0`16>VX$XmYT>h+}$E` zS{<11XW{VK^-k(Hjh%g#K`hif3i2gv?0f_8UrpbsPQMALemSg^!12l3d++*DtPRXF zf^`Z8ny0FBYxG8H79ht4j4^D^_^eR|P`AtQ7C)DfhaRKs1DzoHz2MLHAsM#1F&bp2 zMrz=Wd>ad=e2V%d4Jak2S03yTYdlI$hPP*dPH_?Lx)C@20XqC8p)lWr*zBzC-YaCd zk1Y5-wAoJ<+$#?4zrbu71J60(m4D9kT*BTGfFU1gsfrj>&lg9me&AGb4hDK_fVxoR zgdwF|Y!I2QWq~J=oWv~cQL{ZJ3V#R`g;aXs_6uf9JWPBqRy5`h_Wju~(+G6I!7sF1 z)HyLgMCcdrBECd$%1Pyx4It+}p^Sn(n>90i;US1y)1Df+#_pqxz*9^&-`IraqA|-8 zH?35FYZksI(EMkso|!=J1( zbX-Dvik53MP_#G`js0jedf=5-M6hF5O?3pNI~~UOZ%KrOELk~%@8+G@U75ci=XtKa z0iAAHI#D^29R?UISIeUhYs>wL6#fIP?|3I?O!!vn_WfLqu&SJdzfXg2_ui};VLaM(G`P`u0(W9laKj(wHW)=L%FdiLIyWuKl$ z#v|ncUVvR7#nRm$HMWxNV?pnYVA> zdTafMbAMKUG}+g2I)e-HTzrW5dN!24*8Nild;nUdj6`;pr)YF z7HuN`o`SWtoNQ!!Xx%~^{4wkaAkCsw`Av{fc8G*m*<^Q*Me*Tb)@D_Dl!RV-xS8#( zK%;-{02R~5+dEhmOrvJ~D9sZD4_&-4-BB!K3Xqd3pU}N~wYb6Q_b2y7&jBhEif|oc z9n8ZzbI>ls&brJ6X_CXKWZCTwfe~7xLxk?oqs-6!L)8L>*Oy^@9CrFEen3PH{toF5 z>~&SBtHzdPtB_=Z>@)4XjiagYAoHr&Ma3Au{x#g z^Sk`7P?!ptL3`>DQL@GGpS6huc7uyi|KNkd8Sf6hv-_zWNqMl#8$W$MZlo47%Gf-Q zEpAeG%Na145gvOhB+ObcVdZCM$SlW0@%mWdsHux^EVHkhyEO148PK9E&Caja0N0gh zg+8GqUu$Q)OsDqq@so9!>yFT3h7A7gFnB5Ry`fO<+JpgS?oHv|bV$as`{#C6VPR&M z{r#66J}Xz0Lfoi8grmhXbmt#A?A4I)x}G3cN;?2sjD{_MC2j9w5>xEXp<5dk&cCdA`SWw3AC7s87Ho1>_qt2+0OJWUR}F z7qd4wjXJ4pf?&=!>dfrLpqy)!Bg56of(q4)A?;&KXAN=!O13JrYWpR=uXNB*a?9>$ zx5HJkIB^wG+8GKsGCI|gw8nVab2Z+S##*7o25HBxo?XaY6ggNiAOx?5EPz?j3D7rR zCo7>|>1(-CP&sw{8GhhKyu&(_tYZI-f+)(A6EEWg1Bpzl{o5TT()Hfc&ZJ&+;TGm= zm=kn}6y1;lGpYLWsM>H8x8jc^yZPRYEeo$;$qmlw#`UAYWiQbcmDuBm6E%)UB1*Mz zBT@y*nE}h94&m}IXF6TL;tk&&KWu0kW722rhf`-^e1Y+VBd+WL?Pji6_V6FajjjPYHI;I@?-chy!m9ECf4_)iw zXL3v%-QcrpJkmHoQD^fUa|EAODP!5 zH|jKQ6sO2yZIHxWc9P9Bdo;ayCCJo!mf)p|=XCI)9a+~{dW^|+%vFsg_{ibvOt5yh zjyT_7Tegm;9Xq^lnY~4o`RqBF;6EVH7e(jg{pto95-gLYGDNZXh9XYAyuLo|Y^}bt za!G2(>RqqFlD6}bQ@4G==@(9z7B%{%g^)nMpj;@_ zM-OjFdzlgq%@#AEN{LW*!h_7{t3v+oVtIXT$A-P(PaVJhlrDn}DH#i~O z)90K+yXFiE9w2zdYGTpE)ty8essJ!?SHtL}qLtT9nXJWn2IsX|iv3Qcz#M8%J7ybs zc06ev@a2QU5(_BoNbJ!*ctL}M&Xc)e+?t-L?RV=xL)B;MB_GA2s}}w?wq}Po-YjV| zc%w)TZh^b;>_c^W&rV(Bt~uP$CH2CP=7Z>ZN`kfhUSN&;Mm5j}G<#-OCVQ5)q(x3{ zzic!iavVMQNyEQ#xBD$c2C4nFXyu6;MgYJ35sv1CN41LsakDXo>l*z>FA|v5tqki4 z;I)?QthFkq&UesELPJ|F-ttLRH$6hy68-LmI%@Znfn$d3nhOKz!MYq3aAeS5hL(kY zC?d60YUtO7$O;)FeOPhxs-pTphpJm4J;b)7Y^l*4Eoc7weB{N;>|x5ZWXj>r*>N%c&oLR4_#5i8Ppkq6#@YH{Bf zzGEZ7R#b?`*681@U*6Be-@K*)w=&X_vr<@nvBy=J-dMtTr$T$?Jou-7$?qrBW?pTL ziXh^!N*p@tm2m8Rf?#F*>3W=>0(c`>-Dw#f`qH6^0F}6c)2h9HZ;9}5d$_N@TnOCA zQ}z|fq@Nw5?b;9Zwpvzx=s4_IhXeiqeO-wVzXue7l2qO4Ud(9C$rbZ@0Mn8x< z-Pk3@C${)dT|c6InGF_9Q?-)RbK?)=XhK4i0{INP8wgH|odoD$H?I>?h<6}@j1`b_ z$y1>m+!afQ$e5+=NjQtxUPWpoDB^}(&=@oeMA9n4!Iu855gJqv&OsXRd}SmYALmj< zy_~5^3+fm2V@N%y*0GU6LwD3WxZVr()2=ToJb2XMwos)bfR9|{^5D1fm5qpJog=Ta zf>OJij1?Grk8=w(qEW1nre(}EnaSLPn&|wuvp>sKEy3~fs5RHRw2D-k=kuHZ!KdnN z*{0&ZY4R@#1@Q*4Zz|3qn)f%;^@I#8-tVHpX2uDv_@)mctj{ZE?W87$uh8nHyp6f= zv|&MW<45eCZ?*%6tLe>4z74^D#u_y<^$s{P;MKX$*42*4#@D==U%Kx^dGP`qaQ?yplFy z?Q&16Tht9F!ZF71D@(L-e=?~VBBWHm!eR?+a~Dgd9s<8&woC$djvP6kM&vG;{Hp{^ z;ujVvK5zmvgQ|Lh=lcpo|283dpM# zJW{6yUfI!sYz^a*{=h$^FNtwgbl;!Fu>BFn{|ld(kA1mB?@njkaascID9=olvLyxt zj|`m4)RJ|=REcD>dU0uesh7o5_f$Xgs>L$Uf;>-=SA&;$qSU$M63X|2OKlRWHz2nd zXhfxs4AF{j$6Ay+IP*I@<7T`9>dgE^=TG#HjW}unMaJ;YF+R=Wv1``T49@Py>vfu@ z?EJd!(u+K0Ix8`-28RJZb+#e+t5&9X}T3kQdSiH z(h+LLt#Dh#+7ZkuzuQD-6bVOp9ikCq{!hE)Xv%G*!KUfbXl<4$`3ru0QVuUh`4$)E z)8Oy=1WTA;`muu=n7_@_PXB`3)jHuqOt?+ZBm z>*1M3kL^EBr$jJiuY|aNd~A3PI*EF zpRzEB#$WEQ8)%qBbhJltxcL@y(gmtHsm94??xV)aRg*r?IvEqUbom(N1e;HPPDkPl z(r<#c2Fl-IRc{Gw-!$ZK_PdOp4cwRQ9lhvA*)PsmTGm1eV(N<&p_U*{?P0GZ&QUwE zQ_+n+o)d!62M~xIm#PeEjP0npp%YYyN?Zs)Emh}h{v;vgyOhrOBhbNjOM~)DJ<>5- zoQ&6mG`I1qxF?yv#nh^gYXrv=#Gs>_+K$ z(_EDumf8%%CzF5Hy)>ph3N7l@VX$#j^V|nf#0Xk=^?^U3kEnQHMjfO5lcQnnShn7Y zPFOtA91_&N#?@y3Vn{h0vc+ z48|21%?c|;Pz+z)p_$E@zF{kWNncgL+7OP`4+;f@3*6G;Ua?p9g*ISROc6 zjC2v9Z#79*7WBlGle@O>t#b)_no#w&;cn1h7dG@ zW6x#3Nlb?-NIfo{>3*F{GJ4!H@dvAHaln<3sg0PXcO;%Q1>p~F&Zdgc!*j0g`=*LH zntQhotkJKO;Jg{qrnL{&MBh;9k1(&pSl^mH8z^&D=wcq>Q<|C|9fwGCCHuI;W!ro!`G3h9xBCs?vGw(Iake9aWh>m! zIM;#X?+Bte8EQ{yDYhNJ8{@2r5!z{HS znVL!a$gN3N$o*)z)*HI*e z*G4*FJirs7C$i^5m~f^|%)KJ5me5lNEI@4i@q?4IVCs=3ynPG2D&GasCCR=mSix|_bePF`~dMOizRqJa?&_#_R3fpL`l^)(S{1pgp| zsFot{#`Va>AOAhzzw16WTQ`jXbg~d5MZj=EzeEh^hy%$CW&3RNX9PBO95~OkV4W-& z%wP^*rbZ-F{RVj}I!S`v6x^bs=*S;@uW|FO9Bt?cIZ>9X{Zph%e=b6@VQ7xllJA70 ztQ+yy5l`Su>f6J~nMKYzom*YTlES?rT^g+5X9R7zAw|bZB>9EhL6%zSr5m%@(Pjy$b%?T@VBHn==@8q`&xs;5oEn*0flE4o9_uhQk8^FRf(>D?++ z+V?!Rz#1|&1D!E@n*tZf`Yai}iClj&Zg?psRFV8nN>4H;ZNn>Bc9UG0&pY-vo5LG_ z1fsUrwnc;W#ozN}zi+tM#_JR*@MrN`$p+0!%kFgYzQ$cIk4#A)7BP zNLVB7&}%%viWA}ZJ>C)JjiCq4OAPy$goRk267Mz@e3^84R3lxmlSX?7sPaCG&V`Z# zHD5W)319G^Z7Ita#J#|Heajt(=;pCWbQ5CMfMx_I!XsV@*Dp%OExOl&re2_TR0>J= z3{9938U|;e{65xVii8Ed@P2U@yV;n+&urac13gyVjT;Ke{nnRS_js;%bBf2>#VU5KRJdW4Tosnho#J`^00xggDhmp=pq-mB3t1a=ei z3hB6-lmukzR^~Vx=tf}Ogxi!ai0DQKCxv&uBwkJ97gXaOvE3w@beb~=mnu-gAyjRD zbIkkpN!JjBl)%qMYl^CexbE{^UP+wtpz6N_l(@R9$<$B$;jadjNqL8)nH>qz5`(?8Z#Hut%Lu~w zMk5)VUj!zY`?qh?>uhr3@dlYz(`2JoL3nlBZ?(O>q$@YikaZ=WEOTY9Us+{EJQjr_3mQ|JouFXH48@fbt7~nN`C!;~vV9-` zd<)!Q9SS|HB19#BSjx39E;?Cb{^tH+GCH?~-b%uLTiZ3*$SWXbKs%r30_E>YjKeo3 ziX9%@phaRd7n^I`17NXTNPGL|N743!o2de93G%B5dMt$bt)nvD0eR52o9~;3!{V55z+OBQxbqanf)FP!@Qzh^}P%-P&A5bE{wx zNF9AmfltwYPao-)JH(nY-5JWSlK#sf!m4T0#W|kQ-IwyPT3`HNU&|wDDsx%OwA9Iblx^6oK28-T-@S zB{26gdj?p72!li9UeaNOdQK%51Q=%C-C(Avq_Q&p znwrP&aJ|TemyG~3DxT%;Rd2VkOUj$;S72R#_izz+d?b+>#KEi(;+XGR6f|t<&|-Wd zLU}I+$%DKNCFvmLstJ*C)_avoF~cABvz~{VooD^Vp@o8ZYzKchLb)05&-H2{m6UGj zkr^A)JsM4itHss`(|f@=@uicg*nz0}HCqHS)jL1?D^>j82_r)}$sgD%iOOL3I?TJ- z?G^QGXwuzsrJTzy94>-Pgms8Ju;!0UW~2Ms*~c{?{XB!}jDooRhd$#7L6zcFMuZOA z_m=NLexNq1qZ%jX5Jb6kZ;(}1i6t<~CMta3o6J(;9?#s`Zpb);=X>Q#8G_^zZXvTl z@k|1-diKs{p@_H=UrcjImY-~!tao@;==Y@ezDxfAV+qdImWOnbT-Hb4ml_YrrI`!< zjv^Ltw3C=RzZ!%^1qA$*4K??_kV!omo*7f=Mf`ZTfI7a0gO{9`90P*$Z`8NSMa^tp zjWiW07^vfd_{lAUFf7Cd9Kgsv^gN-kgjNrV56J$~Pzxi%Fv*y)iuJ-S2ld9q(+kq= zNVlbZ^9De-CC->PDubTblz6wm;>g*OpGdF_)6zt-D#IP`?}o;37wJ|;^j}?dc-s?^ zL=Xn(r6xOWOU5i4l8hDkO>n2N3R%eNiWT8qmx%^?Y79~C(l?8E0)0w;3;{sLg(s{B zZV4xag*1keQ7LdS46|B5UEf?qJBzXwHCRa5<%$Pq_dQjcp?>jWVo?X`Cy+@$-8tZs zJ`QzW0nzHbShWE4sft1_nz4>Wc=HHtx~*(7of0Jcn$<&5qO0@??jwoPY`ra(87BQF zxg`XS5Gd81Ac5W5_(nCLuJzG=A?$1)1jm`d4>g#iS+y8lBDBvAWv0+Tgn2H&h!qWn z@h~dQ`yqM(9w60igN1u^i7oKy4WY>jOh$x@eB?&3oU2WjI&fPhla}q@8)93`GHuC6 znI7U$9-U$R!$uy=a+d?hWvYD)x|5+~J4shd$Lgwv@wXyK^^e=NKxd1twpta$ z*cxt~vx!^HC{=Gz0aBO1=GLt<)qiI2;bL)l|LHy3kDOp9@2d>CS1Bx#?3y!>s@)Y! zZz)b=!r14v?4{?w)sNcPX z&Zy(s*=SUtr+=Viue{{+!3}Ro%lVjkZv>o7# zIZsVo9zBYXOmFw`Bo|SREl}R=z&mMh8OS|e6iOIGsU39jMeS~=97Ezm= z({4eW0{w?I>NP|A3^e{t1$CXDx*?)b(w&iPJJO8&vncgWsg|ycczbpC=f;Kd*qT;~ zt+2**r{~<2M)Nrxs7wxK86a8B#AA|o685n6B!X$k)?UhyjHJu5?@|47ukVnG9f6Sj zF8uz1-x$?gmJdaq4rrI$I@k$J$E zKoc-agxUD;y4X9@VIS0%b6jHCW~go~N$jiW8dc(c)>y(|yv#|}mW z77;);pPZ{#tZ(R@R(hlZ`6b?llb7XCGvsWXE7{zVx)`2|f1sE|H~Z~kkq|c2eWIr- z84jOi{7GEg%es>94xuZ_2piEIM)|Mh2W$oxe3=J`KXh&$=l{^sNy4J$8ppl&d_mAP^IL8#X3DqSM{XhLqH zWaj+MRpXK8FX?wV!7hi)r)w_L(h3vk8GO%N(j8_ zUtog%wV6GLcT%xRW;*0?stwnCAzl)PpZXa@$j;xTQOJPuHiz#W#`>byyt@Dln2-ki z$K1=bR~c{S>{9%ufwIX{&Ek;P<~?y&7&zCo;zP)>U!0@8h=w2xabzp@#Q?5i;IfWl zRF5&62sCl-Z4#wZ2gJ-i^z{x3TBSXiO#7UZ`it}W-)Zs{CZNldq@HdOp!hqwyb7GN z*c~`i?7F0c^P3%V_Lu!8KIInj$d+713f{Di{`IUe9A6!FDTLDGF=&tW;NUfKUHs2m z%&P&66A48|xNk|DP@JrVR{=&D4X&(oExGjRmp9qg+y{mtgo6EC8W=%7CckzPf>%_O z0Z$^0eY{x9kBHy&Udd_iYf8*#8xf^PrD{M86u&VB2KBND9d1rf?wPamZPGED86f{JvM4Jz zjjaY$BjtTluR(cPc#%fG8!of^9+>lLMTokId#fWn6JjF$5-ICLgZeHP5!H(lWL*ep z3!nkY+EnqbRTacLiaYWY6hsg0FCeI)NVEjc%n>_JMVV?yOt?lWp2G&X0N{2$4t?W3 z+6Un;LrC5T4DWpExOrcI{AreFpF$}FWRN$ma0s%cgilt)=%QH68$3EF{46iw`BU%T zn{aQkRn;hkg7<-Q_gF22=@ryG@)3rj?;P1l=A32<3^%WTvBH9CTyjW9Ue`vC>#f3A z=;|ja?9(S3=q_>XBwSAGMk^&n(`d!aoqE=e@`st0BD8H}&-aj0w(BtSEL&hiJh4uC z)SPgQ69N2Dl?>P$wJ%x}gz+Kd&X-ZP3|;XyuPU=7+AI+lgk?q}NRIiM6#aK~e13Ms zt%fhy`sr1tYQRsk6%mXvB!J>T>(!h45}|1kL_NpP;W|Dyvkuq1+X{cT;CvFL(TINx zY|WKJ4OyVF-bbKG6Cj6cTJB&kcur<#`Uz7Q{TKf)J>tlM^qeA#-O>v3Mipb~`%eP< zAr`;;P}1EkjscU)io}iAHj$F3TL#F6NC-u}JtbwM3t4sIR11(?93dB-w**z$0PXdX zLLcM8WtaM_uj+Da7F=s4eccK7uD!7cqmycxa0mY;HEj&in6!o=PGeES zMqyx9ZrxJo^m{un8Gd+8>r8_lBT#-4DrXO8cB;h769}HJ+Ssq{Ko_gTR~LcbB&`S* zg5wBphp}tph+OZsL^oIS^n2N6;HkKa8<Pw1*WD^;l57X{O@Fx0%F~B713jd1Hs?b|bQSWtlzVB_;4PvqgL@6SSa8rJ2 z0&e% z_uFmeBH_ynBW^T}bD;+IWL%kO40f0oY%xHULj5JOeizf&sGb)pTYbd(oA(3IZ^abx zv2}$q>a9lZ{TL)6xHn6fB~V_chdlf7=@-h^un^Ej0X3WGsbpjS38PW|o9;4Mtz>5_ z_IE0!FnsR2lHJf0=_@vkgwyEXnF09+JfX^K2cc#1xikF{kIRwmR+V!et@zV&o$qQ@ z`8vzpzgVaRbkRq3hvnhsrp$Lv(3ieCO{8l)yN?vOLVR65t$~Z$X&U#bT2--hDQz`% zzsC2wLO(rS523Dq7!d|&xcO2ya-9*6^C{s3;~$JK8^UG=U4&e^QwZsx-J;qRcz9p8 z@5|nr?u~}x&SCF$9q6Gr`7(MwY`9Nm6}~gFfIW%zt-aU5|>wtTY2gDp6Zr2LDN!KoMWTv)?y3c&g{ z!>u?p0?C~}5@3`!wvAFAMiZO##G?y+#y+t5mCX?zzLiJ##XwI59hoginex^>{A*aO z3^Qa^7Dx)1dGJA4Ja@{k*)jbW8@~bK18XRzLUL2JlUHendD1x_jH~McYQML6ohC{4 zxF_ZncpRM`fg$|z;$l$8h)JDq+7BXdg&OVX)A9aNS^q+NIR7@o-hg`2yc9%J9#+18@>x2ispdV|FSYWQ0Q!4{lNJ2X^rq2pB(8Ok^-sTd zDeZz5a*OUX;|6M z!1BcS_kk7eEi;fQVRrE>sx5Yim73_^jT{vmuoDp;8gSyzq%1b>2zVK4#Yc8HE(IZb1ix zWvng^D>qT49>G$sjH)~Vx70s;$EduKUB7~Z(5JXdg@sFy1o~B9v!SWp4To-zST{oQ zCNbq^RrblAGAwytovHis_NeS{5{w^-`M@`lfn}F7aA#gNB%`&mQPFqC=2R@cB=7qB zrU&}Mg`pf}IoO^~!GWM?NHF7LdZ>?G{?%k2T)>I#NXo3wCNfY@8c8SE65lMq&_3e+ z7FH{mG`(sO(>HF+l#cd3xl8ZkJ-q!65{Z#OhD$t`>5)n+_6R3B{b&~x0C_vw2;!IK zi98}&-K8pf;Io?R%dC?wj7&6O@c<%u zPR6QbtRZj3^u`{pO}yy6<(6->wpZ6NVyLTpZ)9*Jt2+{9!(l0K5L9^Urm-4onn@P7 zt@q{PmEu`cJ2@z-2yGh{g^3uF4jvq^ygVG&LG&e>R134>hBIO8Eq9jDt?6yU7wC(| zKQ3zT;*=7hQW#}1U{Sop8i7$A^~9N?nHPN(y|GesEtVN_su^P~@F?aiY1`Cfl%_j(E#2z?n()q zGnYx<&zleqY#`HWgDOm3op|}9Vz}MEh@r#DJ&sl`u~=FbODUy-T0G5)7I6&`!rwQ{NcVeQBKS*aTJaQ;hQ61~ zSl_&Jh)8aC=xrL|)>Z>sSbl6pTyRN{IWzO_=<n-?wy zY_k#@Bu{L*%Yk1N~!}?s>-Nr-?3@>2V8e?4+)XkLA zBN7=PiW}U2iP#kV+Lxd2x4xa#=5kx6`I#y(>g{_OSx_~|aB5yQ&)?CUB#w(|DI!bg zb$yE$Ad&{sg|~JPGb!s-{YOhO{u1<|B~2MambAM*p+INLDa%`nG-H8)l7}(c3Q@vo z0#90RJ#xk3K!SLz6?oKmX$yco;&3grD=d?5_?*8pjYqZ0U;h_8O5hvO#R>I3~IJK;+akaehnsqa#Q95ndVE7yvMo4y-ZKd0dG{ z`aYm}CDh>m<^aooo!XhCkOWGPM)%8{#Mpiq^Ry@a&ZNnSyOd$v?SU0u90BBKR|swu zO66NqSWsDPN!&jb9^&E~+@tF5H=lpAZ}C7LmAlG}A4^2!9#5o20iR+FR#GPKAM>3;pJe@ao2HW3%CZ|f_o%+O zp5XKMrx;m%<%P{$u1OR^O@+#OL2y+ zcKOwbwSgKWDm>*jG-M6v%HJVR+7&*3@=?+JS>zWV*uxH$4>3I8^OF`uaNogQ<%G4q z2K{j$T^X$Y>~rd-h^x){vYJ9jQ{Kt~*=Dq9Q~BuFe1;x~981jeecqZ4)ry}AR?lB; zt*X5QXY>nsTN85d16s=C%AWMciig=?D|S(Xd`PniNIIpK@b#?n;I_1 z;C;X0z@n#vbHlXZGEDcMA7P)CMT+;0e|l-&^O0Q6!)N+l#HH}6q?vrvV~NCSTBs&t@Im zW#RKhBZ`e9x_Gv^998&>R;oBEe=1m-9r4aNMp#fOJ-4=cekEko!6@DL zl2t2F5o!-7`Br_llepyRa3Eup3@X2Jl~A_+GP)>72Uy>(t1lA_0@;#{%wd>bp9H&l z{aKM}wgQ)+*`~xZ&b5|hd-fiKl%mRL|Hy4rkWNg?HV~R!lH4ww^wSpkcg~x@xLywK zXzQc0c9Df?HBNN?prIVwK0L7ey8YmDBc7G+P1;i9`J5z@g7X&qq?=Um!?cwve5Cbf z^1PCygu|o)@ctXWgqdyBOtL;!-M9-hIaT9MK|hz zkEMX6%|a^n%n6Q0^7IRXf{P*Jq?q21p3lz`%4V*j>_E%Uym4cFds5J)bu7#ZsZ0C1 zmCh>Kl)Gr}O!f88N(~4Of0V103QJ&wy>dz^Tep;AL~Qv$N+$$qtaLj0h#jC?0%WNj zUN7DGU_GF4Yl27rK6I0;Lw9#ZyzjB%SH2V*OR&Nf56|d=xh{$c{<_CeE$B55{OMK+ zxr1~yV$B!L)DP%6LP5qyHv=>pA9GmG0=VfrdOsl?uu(K z=#u0vf%Fu4k?S8Nt*EzVVWw~&TO=o#jLH%C8>T;Y5OSa zN%BHa_+d3C;ZET9kBrFrl{CyOfZ#M9l3ME?6pM&^Io>Q40JkpY{0@PZ#8Kvj0&U&k zY&gvl-q1@9yr!FT78mdKmRp8`;6iz0PZK$Iix`)4dVZfnT)m@Cq`g>$y#|g-(s+(T zY4T>^FLLGB?_H!E`GowEQ<~41-EvLM<(Zb6gSi~Ds@ldNg_N3avVq^1-Z79Z!`*9phm|56Ps5USx;!_H^VU^sFKV#m`pap^?*fG93tB3(55| zJ=*Z)nb~nhb<{!)+ssp(k2YDR!o_51xbc$?ZJoFOXz$E$u8#%$DTOJ!QuaNRQM2YB zFsu>F8<2#x(v_2_Dle*_#)JNB|7*o}@yIYe{+pL@@NAAyroZ4JVcJ>uLX480V$Xs@ z;uCKzVpu5|sj|vHdezydKi6RzL9!xIL$HSlfRmRg&fNghJwyF$Om`^|-mw&~$#>bb zK1%hx!n-;1kBZ@dsxIEX#MCOmdeHmY(mLihUocemz9(wuI9u8{CO%o#wsI5qBDawH zjG)6c9p(ya*IJ!#Tgm@7f#ddJxK%5W!a}U5M*#j9b=?MlxIF_12EDMX9D{csdy0V! z*jPw(CCyK&W^^z9q=UZumehCZhWfqqhW*>B-(jF~j@7!-vsv>&joky(wXiDyfeu<~ ze?vaqy<4H41k52b#ov`EIwc~EY|gRi$|o`T1@*Fe1OMcR*4g>5==ml+wtD{b`iWR6 zYplHaKRyGSv7yT;06%owvM*19$c)Y#I$yjx)7;lTgaaO?I*!zWfF}?8i(b!+&*lh- zH4mSpuNCx<3>9{aPC=$4HZXDx^;DeuE-l*+&mW%|63oYfPBrJHHoi4Z(2`7}pzkPW ztjZA)mjc5r6GfqQY?ZpuW5|6zFQgiOv3O8Y%y0bJ1TA}ogFTyS*Cm|Hn)|aUVi+5s zO`pfhBT2xz_ul&Yz@1_iRtDb3Et=j+$*6K5n_>3>4VH7{VS32rhVecO>FNUh52XP4 zP^ooSs6@A_WXCQ742yf^Mh?Gdo()zpGr7U7S1en`yfHkOwv|*JPgY0?j6a_c{$%?5 zDLzWIjI_E6RHqf!jz(ki%2OBH*OJ5aK%5(?hn&o8-Kk3XpqT1g4WVM=o8E@74Hm0< z#L^>!3Pm+p?Q)s5oCT)~ z`jcra2!Fo&7utT3eZ%OtdMonJ~9*hkf^&q@rJDa_;n zCj&;l$zLnAaSPDK-LGHePDv?So}yKKRt4BBhV`17J*5G;wj-4%whl_UwDl{i@*dr?XA@#+u(Lo z`2i^HsorbsG2Pm#n6@fvIXl1kJmQ@#2E4(G;k)jlmSd^uwSHpxl5hvmx7lsw;=z|v zW`I7}k>#kQpKj?PYBcmwscCn`auOOf`dIQZYylrk`EX|U0?K>DDSgaD7$ccUNfn_| z80TfL&V$||d`1y?lMAC0n>ljt9a~iZ=mT7X*yuXC&C0atDC>oj`c>`zgThGrpVmi^N~6Jnt(m9kW$}?beGt-plmI28@zO zzYP3`(Y<)zwjXSk=`>CWauSLj?K`;3a-AdbZkZF4K4OL(4+U>V&_0sCoRe1BC|^Nb z<>oMZ|KUZo{%suNFUFU@zcE&7bBB6)xP+&iBbjX41`Sx<7Pw`n(R=kG!N`T4F*n|- z1uyQIY3%RKHH*SL7sM>=R{8JQ5yz{iy&GJmR0rC$858X)smzUa4inR69_FSyqr5Eo z^&Va}_@w=|0&2&ONydoShdjkOilB-17B4jVGB6$>w=2+vSVxZiFNm52_JHmU#k>Y8 zAno~{Wno_b?g3V+5AyH|-z`yZZ8IrQA4r?%YPZ!|efIw;<=&&2@cTG`o0T$mnh3dt znE7o&8ww8^qFk~lD{D4kL)$X9Tw5{O%DquULK`WO7P)4Rrnz<#Q3z2YBqPuI^ZDcX z>-p<@&gY!>`JVIrobNfGe?G4u^~Z1BhWvG>74z=OBf}%pB4wXkj{`4Ywf8??0kKNy z=A+TjKj-hR2>ecoLaIHtVjgFBdku3-p46Eyo~}RrR54&5+d^dFG zpOoDQZL{ue+`C)(`B4?om`6)3Vf1yN_r-1OT)||S?vc=XkZJR?V84T}m3My7EtL}a zB|Q&_AA7o20^`W0`=xOPuTWx!As>A~z&~xB0jj1}g1LqdQY;hKGgd}Z8@20vF#m^J zr+q?ZWQ%aAPcfdJ^EKL7>qY=%IG&|U`slFlUPe_h1X?0>tjoqNil_mopr}ncUXb10 zY4W;Lr?Gt!C**qf7njQhKPypSgQuQK2y*$q;sRb8j;kT^4VSzMHQTixC2bYKwz}K0- zs(r1Em+-~Ocn@5#Kn>$e&M4oJP-a;rx`~|exI!A#Gv7hOw)CAPI&#>^j}9!Z5wU7t zj$XF?9TA?I64SVF$e4E_ZQ;`ZzVF7)p(y8>DB`k7HNhDeI=;<=K%~HKDSX7Or66AV z1Q|>m3&u72zr9h~RjPRM_gXp~(acAYgEsq}n+K?`3g^l>EeXm`eIw|7&s zx!!-ANTZO1EMwdIWb?zdzX9+~WcC}Yk}p2MaEhlKukS2NEBMRVkoJdX5gwxxj^7Dx z3ZAI57^vumOqkKpM2$^k-8^yG)3)ZKL$86G)Uo^#3-mlx#^rRdN4+LPX!Zk5z9 zDfIxkxka5}MCj^)JW*0dXGPEhQo+ko9kZ%@GD7ty@#2Vk;`f50Vof|+K9#3?WWQ?G z%0^~=Mza3BormmNUB>l#2C@<8v2Ki8&tzPgT`+pRgy6Vu!J7(YELtXWm=+aH`p7s3 zenW45?i6=ps+{q%-+BZGT6qPM8Jg;o?loch>#9y%^LwO@3e`JJwmqk{__h9B*1kUv z)kGOr%Oqn1YoNnD-nM-8QY^Z0vP?NPBt2hJPxe;7^W5i*pWn8tWoGx~&A4d9j=e8P zqFFAEHF9Gp9H}3=Q(9x6%83S5Z3Cx=vW(1WE6?J;^-j@V?1*U(^CLg@kNWDG)~c=-@a{r}FaC>9etG-(@xL_A#eDw%-yLVrku7YC-dpLhYeifou@5}2&@m_t4!g=53Y zq3!D8jhYWqGru+&R${heEy;|a$4h1co3OxT$vh(q0hb7jrERbis40hij_yJ3Qkk%< z0@vwDP_+z5o1WLnde+Av8+aSx36&`r1Z3~H2j6q%XtQgqF{)cXc;JlsrJI1q{$3K+ z3fV;ak(3Ew5a`ZODly1$PON``?2GU89FFffn<#B}#Hd_r?tmeZkmv7ZANx-^I1I{M z^(US5lHJW{`3}3>!L5|QsIp8Vi>61PlSL!Pt0w$dW>Vs}Kv~BsQyOU8k=b&9V4Z4% zPk5ae-5=JH;ZMLz4!q8BL2)Z;vy7Uj-?$lAP<9@Nt-j};CoM~}BV|^b~x4Xa=5Sjx<9))9O38NB7t=u4$dT~!r&Qrl;CGCy~#@Ab# zqBVJ18AM>T2T|y2ZjX_N_9^(l6a+#{v?gKEm}xf`Vx;r*cYBtQr|mo&BgmSSsEcN5+c}33Hga1r%-$ zg&|JB;nr}3wWS3F2D662G_!_z{}mub1ceY{{&#@XdrnFmQ2%>_PXvKV!; -
      -
      🎉
      -

      Congratulations!

      +
      + {/* Floating celebration particles */} +
      +
      +
      +
      + +
      +
      +
      +
      🏆
      +
      +
      +
      +

      Congratulations!

      {result.playerName}

      You reached the target word!

      -
      -
      -
      -

      Time

      -

      - {minutes}:{seconds.toString().padStart(2, '0')} -

      -
      -
      -

      Moves

      -

      {result.moves}

      -
      -
      -
      - +
      +
      +
      +
      + + + +
      +

      Time

      +

      + {minutes}:{seconds.toString().padStart(2, '0')} +

      +
      +
      +
      + + + +
      +

      Moves

      +

      {result.moves}

      +
      +
      +

      Your path:

      @@ -51,10 +71,13 @@ export default function EndScreen({ result, onRestart }: Props) {
      -
      diff --git a/client/src/components/StartScreen.tsx b/client/src/components/StartScreen.tsx index e4ebcef..e87ecf7 100644 --- a/client/src/components/StartScreen.tsx +++ b/client/src/components/StartScreen.tsx @@ -41,12 +41,41 @@ export default function StartScreen({ onStart }: Props) { }; return ( -
      -
      -

      +
      +
      + {/* Logo Icon */} +
      +
      + {/* Connected nodes logo */} + + {/* Connection lines */} + + + + + {/* Nodes */} + + + + + + + + + + {/* Sparkle effect */} + + + + + +
      +
      + +

      SYNCITY

      -

      +

      Race through synonyms to reach the target word!

      diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index b955e8e..ca46f31 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -398,7 +398,7 @@ export default function WordGraph({ source: `path-${path[i]}-${i}`, target: `path-${path[i + 1]}-${i + 1}`, animated: i === path.length - 2, - style: { stroke: '#791f3e', strokeWidth: 3 }, + style: { stroke: '#791f3e', strokeWidth: 9, strokeOpacity: 1 }, type: 'smoothstep', }); } @@ -412,7 +412,7 @@ export default function WordGraph({ source: currentNodeId, target: node.id, animated: false, - style: { stroke: '#cbd5e1', strokeWidth: 2, strokeDasharray: '5,5' }, + style: { stroke: '#cbd5e1', strokeWidth: 6, strokeDasharray: '5,5', strokeOpacity: 1}, type: 'smoothstep', })) : []; @@ -423,7 +423,7 @@ export default function WordGraph({ source: currentNodeId, target: node.id, animated: false, - style: { stroke: '#e5e7eb', strokeWidth: 1, strokeDasharray: '5,5' }, + style: { stroke: '#e5e7eb', strokeWidth: 3, strokeDasharray: '5,5' }, type: 'smoothstep', })) : []; From 96823af51bea33bec1b3b49a76b42638b4b655c6 Mon Sep 17 00:00:00 2001 From: Kenneth Ong Date: Sun, 18 Jan 2026 06:24:26 +0800 Subject: [PATCH 17/21] added node library --- client/package-lock.json | 523 +++++++++++----- client/package.json | 3 +- client/src/components/WordGraph.tsx | 916 ++++++++++------------------ client/src/hooks/useGame.ts | 20 +- server/main.py | 2 + server/routes/similarity.py | 2 +- server/services/game.py | 1 - 7 files changed, 705 insertions(+), 762 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index c745982..2fae07d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,9 +9,10 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.18", + "@types/d3": "^7.4.3", + "d3": "^7.9.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "reactflow": "^11.11.4", "tailwindcss": "^4.1.18" }, "devDependencies": { @@ -981,108 +982,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@reactflow/background": { - "version": "11.3.14", - "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", - "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/controls": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", - "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/core": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", - "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", - "license": "MIT", - "dependencies": { - "@types/d3": "^7.4.0", - "@types/d3-drag": "^3.0.1", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/minimap": { - "version": "11.7.14", - "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", - "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-resizer": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", - "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.4", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-toolbar": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", - "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -2003,7 +1902,7 @@ "version": "19.2.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2482,12 +2381,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2508,6 +2401,15 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2541,9 +2443,99 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -2553,6 +2545,30 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-dispatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", @@ -2575,6 +2591,31 @@ "node": ">=12" } }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -2584,6 +2625,62 @@ "node": ">=12" } }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -2596,6 +2693,71 @@ "node": ">=12" } }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", @@ -2605,6 +2767,42 @@ "node": ">=12" } }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -2674,6 +2872,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3123,6 +3330,18 @@ "hermes-estree": "0.25.1" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3160,6 +3379,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3813,24 +4041,6 @@ "node": ">=0.10.0" } }, - "node_modules/reactflow": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", - "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", - "license": "MIT", - "dependencies": { - "@reactflow/background": "11.3.14", - "@reactflow/controls": "11.2.14", - "@reactflow/core": "11.11.4", - "@reactflow/minimap": "11.7.14", - "@reactflow/node-resizer": "2.2.14", - "@reactflow/node-toolbar": "1.3.14" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3841,6 +4051,12 @@ "node": ">=4" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", @@ -3885,6 +4101,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -4106,15 +4334,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -4257,34 +4476,6 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } - }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } } } } diff --git a/client/package.json b/client/package.json index bf63d19..31eea37 100644 --- a/client/package.json +++ b/client/package.json @@ -11,9 +11,10 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "@types/d3": "^7.4.3", + "d3": "^7.9.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "reactflow": "^11.11.4", "tailwindcss": "^4.1.18" }, "devDependencies": { diff --git a/client/src/components/WordGraph.tsx b/client/src/components/WordGraph.tsx index 5d50d2b..a9e23c1 100644 --- a/client/src/components/WordGraph.tsx +++ b/client/src/components/WordGraph.tsx @@ -1,15 +1,6 @@ -import { useEffect, useState, useRef } from 'react'; -import ReactFlow, { - Background, - Controls, - useNodesState, - useEdgesState, - MiniMap, - Panel, -} from 'reactflow'; -import type { Node, Edge, NodeMouseHandler } from 'reactflow'; +import { useEffect, useRef, useState } from 'react'; +import * as d3 from 'd3'; import type { WordWithMetadata } from '../hooks/useGame'; -import 'reactflow/dist/style.css'; interface Props { path: string[]; @@ -22,58 +13,46 @@ interface Props { onRevertToWord: (word: string, index: number) => void; } -interface HistoricalNode { - word: string; - position: { x: number; y: number }; - definition?: string; - type?: string; +interface GraphNode extends d3.SimulationNodeDatum { + id: string; + group: 'path' | 'option' | 'target'; + type?: 'synonym' | 'antonym' | 'related'; + pathIndex?: number; + definition?: string; } -// Custom node component with tooltip -function CustomNode({ data }: { - data: { - label: string; - backgroundColor: string; - color: string; - borderColor: string; - definition?: string; - pathIndex?: number; - isPathNode?: boolean; - isHistorical?: boolean; - } -}) { - const [showTooltip, setShowTooltip] = useState(false); - - return ( -
      setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > -
      - {data. label} -
      - {showTooltip && data.definition && ( -
      - {data.definition} -
      -
      - )} -
      - ); +interface GraphLink extends d3.SimulationLinkDatum { + source: string | GraphNode; + target: string | GraphNode; + value: number; } -const nodeTypes = { - custom: CustomNode, -}; +// Cache for dictionary definitions +const definitionCache = new Map(); + +// Fetch definition from Free Dictionary API +async function fetchDefinition(word: string): Promise { + if (definitionCache.has(word)) { + return definitionCache.get(word)!; + } + + try { + const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`); + if (!response.ok) { + return 'No definition available'; + } + + const data = await response.json(); + if (data && data[0] && data[0].meanings && data[0].meanings[0]) { + const definition = data[0].meanings[0].definitions[0].definition; + definitionCache.set(word, definition); + return definition; + } + return 'No definition available'; + } catch { + return 'No definition available'; + } +} export default function WordGraph({ path, @@ -85,565 +64,338 @@ export default function WordGraph({ onSelectWord, onRevertToWord }: Props) { - const [nodes, setNodes, onNodesChange] = useNodesState([]); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); - - // Store positions of all words to preserve them when selected - const wordPositionsRef = useRef>(new Map()); - - // Store all historical option nodes that have been shown - const historicalNodesRef = useRef>(new Map()); + const svgRef = useRef(null); + const simulationRef = useRef | null>(null); useEffect(() => { - // Collision detection helper - const MIN_DISTANCE = 120; // Minimum distance between nodes to prevent overlap - - const checkCollision = (pos: { x: number; y: number }, existingPositions: Array<{ x: number; y: number }>) => { - for (const existing of existingPositions) { - const distance = Math.sqrt( - Math.pow(pos.x - existing.x, 2) + Math.pow(pos.y - existing.y, 2) - ); - if (distance < MIN_DISTANCE) { - return true; - } + if (!svgRef.current) return; + + const width = svgRef.current.clientWidth; + const height = svgRef.current.clientHeight; + + // Build nodes and links + const nodes: GraphNode[] = []; + const links: GraphLink[] = []; + const nodeIds = new Set(); + + // Add path nodes + path.forEach((word, index) => { + if (!nodeIds.has(word)) { + nodes.push({ + id: word, + group: word === targetWord ? 'target' : 'path', + pathIndex: index, + }); + nodeIds.add(word); } - return false; - }; + }); - // Adjust position to avoid collisions - const findNonCollidingPosition = ( - basePos: { x: number; y: number }, - centerX: number, - centerY: number, - existingPositions: Array<{ x: number; y: number }>, - baseAngle?: number - ) => { - let position = { ... basePos }; - let attempts = 0; - const maxAttempts = 30; - - while (checkCollision(position, existingPositions) && attempts < maxAttempts) { - attempts++; - - // Calculate current distance from center - const distance = Math.sqrt( - Math.pow(position.x - centerX, 2) + Math.pow(position.y - centerY, 2) - ); - - // If baseAngle is provided, use it; otherwise calculate from current position - let angle = baseAngle; - if (angle === undefined) { - angle = Math.atan2(position. y - centerY, position.x - centerX) * (180 / Math.PI); - } - - // Try increasing radius - const newDistance = distance + 30; // Increase by 30px each attempt - - // Add slight angle variation to spread out - const angleVariation = (attempts % 2 === 0 ? 1 : -1) * (attempts * 5); - const adjustedAngle = angle + angleVariation; - const radian = (adjustedAngle * Math.PI) / 180; - - position = { - x: centerX + Math.cos(radian) * newDistance, - y: centerY + Math.sin(radian) * newDistance, - }; - } - - return position; - }; + // Add links between path nodes + for (let i = 0; i < path.length - 1; i++) { + links.push({ + source: path[i], + target: path[i + 1], + value: 3, + }); + } - // Create nodes from path - const pathNodes: Node[] = path.map((word, index) => { - const isStart = index === 0; - const isCurrent = word === currentWord; - const isTarget = word. toLowerCase() === targetWord.toLowerCase(); - - let backgroundColor = '#e5e7eb'; // gray for visited - let color = '#374151'; - let borderColor = '#d1d5db'; - - if (isTarget) { - backgroundColor = '#10b981'; // green for target - color = 'white'; - borderColor = '#059669'; - } else if (isCurrent) { - backgroundColor = '#a855f7'; // purple for current - color = 'white'; - borderColor = '#9333ea'; - } else if (isStart) { - backgroundColor = '#6366f1'; // indigo for start - color = 'white'; - borderColor = '#4f46e5'; - } + // Add option nodes (available words to select) + words.forEach((wordMeta) => { + if (!nodeIds.has(wordMeta.word) && wordMeta.word !== 'No words found' && wordMeta.word !== 'Error loading words') { + nodes.push({ + id: wordMeta.word, + group: 'option', + type: wordMeta.type, + definition: wordMeta.definition, + }); + nodeIds.add(wordMeta.word); - // Check if this word has a saved position (was previously a next word option) - const savedPosition = wordPositionsRef.current.get(word.toLowerCase()); - let position; - - if (savedPosition) { - // Use the saved position from when it was a next word option - position = savedPosition; - } else { - // Default linear positioning for the start word or words without saved positions - position = { x: index * 200, y: 100 }; - wordPositionsRef.current.set(word.toLowerCase(), position); + // Link option nodes to current word + links.push({ + source: currentWord, + target: wordMeta.word, + value: 1, + }); } - - return { - id: `path-${word}-${index}`, - type: 'custom', - data: { - label: word, - backgroundColor, - color, - borderColor, - definition: `Click to revert to this word (${isStart ? 'start' : isTarget ? 'target' : isCurrent ? 'current' : 'visited'})`, - pathIndex: index, - isPathNode: true, - }, - position, - draggable: false, - }; }); - // Group words by type - const synonyms = words.filter(w => w.type === 'synonym'); - const antonyms = words. filter(w => w.type === 'antonym'); - const related = words.filter(w => w.type === 'related'); - const other = words.filter(w => ! w.type || (w.type !== 'synonym' && w.type !== 'antonym' && w.type !== 'related')); + // Color scale + const colorScale = (group: string, type?: string) => { + if (group === 'target') return '#ec4899'; // Pink for target + if (group === 'path') return '#8b5cf6'; // Purple for path + if (group === 'option') { + if (type === 'synonym') return '#3b82f6'; // Blue + if (type === 'antonym') return '#ef4444'; // Red + if (type === 'related') return '#10b981'; // Green + } + return '#6b7280'; // Gray default + }; - // Create next word options nodes with circular positioning - const nextWordNodes: Node[] = []; - const currentWordSet = new Set(words.map(w => w.word.toLowerCase())); - - if (! isLoading && words.length > 0 && - words[0].word !== 'No words found' && words[0].word !== 'Error loading words') { - - // Get existing positions from path nodes - const existingPositions = pathNodes.map(node => node.position); - - // Get the current word's position - const currentNodePosition = wordPositionsRef.current.get(currentWord.toLowerCase()) || - { x: (path. length - 1) * 200, y: 100 }; + // Initialize SVG once + const svg = d3.select(svgRef.current) + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]); + + // Get or create main group + let g = svg.select('g.main-group'); + if (g.empty()) { + g = svg.append('g').attr('class', 'main-group'); - const currentX = currentNodePosition.x; - const currentY = currentNodePosition.y; - const baseRadius = 250; // Increased base distance from current node - const radiusIncrement = 90; // Space between layers - - // Helper function to calculate circular position - const getCircularPosition = (angle: number, distance: number) => { - const radian = (angle * Math. PI) / 180; - return { - x: currentX + Math.cos(radian) * distance, - y: currentY + Math.sin(radian) * distance, - }; - }; - - // Helper function to distribute nodes in an angular range with proper spacing - const distributeNodesInSection = ( - nodeCount: number, - startAngle: number, - angleRange: number, - baseRadius: number - ) => { - const positions: Array<{ x: number; y: number; layer: number; angle: number }> = []; - const maxNodesPerLayer = 4; // Maximum nodes per radius layer to avoid overlap - - for (let i = 0; i < nodeCount; i++) { - const layer = Math.floor(i / maxNodesPerLayer); - const indexInLayer = i % maxNodesPerLayer; - const nodesInThisLayer = Math.min(nodeCount - layer * maxNodesPerLayer, maxNodesPerLayer); - - // Calculate angle for this node within its layer - const angleStep = angleRange / Math.max(nodesInThisLayer, 1); - const angle = startAngle + (indexInLayer * angleStep) + (angleStep / 2) - (angleRange / 2); - - // Calculate distance with layer offset - const distance = baseRadius + (layer * radiusIncrement); - - const position = getCircularPosition(angle, distance); - positions.push({ ... position, layer, angle }); - } - - return positions; - }; - - // Track all new node positions to check for collisions - const allNewPositions: Array<{ x: number; y: number }> = []; - - // Helper function to create node - const createWordNode = ( - wordData: WordWithMetadata, - index: number, - basePosition: { x: number; y: number; angle: number }, - currentX: number, - currentY: number - ) => { - const { word, definition, type } = wordData; - const isTarget = word.toLowerCase() === targetWord.toLowerCase(); - - let backgroundColor = '#cbd5e1'; - let color = '#1e293b'; - let borderColor = '#94a3b8'; - - if (isTarget) { - backgroundColor = '#10b981'; - color = 'white'; - borderColor = '#059669'; - } else if (type === 'synonym') { - backgroundColor = '#86efac'; - color = '#14532d'; - borderColor = '#4ade80'; - } else if (type === 'antonym') { - backgroundColor = '#fca5a5'; - color = '#7f1d1d'; - borderColor = '#ef4444'; - } else if (type === 'related') { - backgroundColor = '#93c5fd'; - color = '#1e3a8a'; - borderColor = '#3b82f6'; - } + // Add zoom behavior only once + svg.call(d3.zoom() + .extent([[0, 0], [width, height]]) + .scaleExtent([0.5, 3]) + .on('zoom', (event) => { + g.attr('transform', event.transform); + }) as any); + } - const position = findNonCollidingPosition( - basePosition, - currentX, - currentY, - [... existingPositions, ...allNewPositions], - basePosition.angle - ); - - allNewPositions.push(position); - - // Save this position for future reference - if (! wordPositionsRef.current.has(word.toLowerCase())) { - wordPositionsRef.current. set(word.toLowerCase(), { x: position.x, y: position.y }); - } + // Get or create link and node groups + let linkGroup = g.select('g.links'); + if (linkGroup.empty()) { + linkGroup = g.append('g') + .attr('class', 'links') + .attr('stroke', '#999') + .attr('stroke-opacity', 0.6); + } - // Add to historical nodes - historicalNodesRef.current.set(word.toLowerCase(), { - word, - position: { x: position.x, y: position.y }, - definition, - type, - }); + let nodeGroup = g.select('g.nodes'); + if (nodeGroup.empty()) { + nodeGroup = g.append('g') + .attr('class', 'nodes') + .attr('stroke', '#fff') + .attr('stroke-width', 2); + } - return { - id: `next-${word}-${nextWordNodes.length}`, - type: 'custom', - data: { - label: word, - backgroundColor, - color, - borderColor, - definition: definition || type || 'Word option', - }, - position: { x: position.x, y: position.y }, - draggable: false, - }; - }; - - // Synonyms spawn in top section (-90° centered, 100° range) - const synonymPositions = distributeNodesInSection( - synonyms.length, - -90, - 100, - baseRadius - ); - - synonyms.forEach((wordData, index) => { - const node = createWordNode(wordData, index, synonymPositions[index], currentX, currentY); - nextWordNodes.push(node); - }); + // Create or update simulation + if (!simulationRef.current) { + simulationRef.current = d3.forceSimulation() + .force('link', d3.forceLink().id(d => d.id).distance(100)) + .force('charge', d3.forceManyBody().strength(-300)) + .force('center', d3.forceCenter(width / 2, height / 2)) + .force('collision', d3.forceCollide().radius(40)); + } - // Antonyms spawn in bottom-right section (60° centered, 100° range) - const antonymPositions = distributeNodesInSection( - antonyms.length, - 60, - 100, - baseRadius + const simulation = simulationRef.current; + + // Update nodes and links in simulation + simulation.nodes(nodes); + (simulation.force('link') as d3.ForceLink).links(links); + simulation.alpha(0.3).restart(); + + // Update links using join pattern + const link = linkGroup + .selectAll('line') + .data(links, d => `${(d.source as GraphNode).id || d.source}-${(d.target as GraphNode).id || d.target}`) + .join('line') + .attr('stroke-width', d => Math.sqrt(d.value) * 2); + + // Update nodes using join pattern + const node = nodeGroup + .selectAll('g') + .data(nodes, d => d.id) + .join( + enter => { + const g = enter.append('g') + .attr('class', 'node-group') + .style('cursor', 'pointer') + .call(d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended) as any); + + g.append('circle'); + g.append('text') + .attr('x', 0) + .attr('y', -28) + .attr('text-anchor', 'middle') + .attr('fill', '#1f2937') + .attr('stroke', 'white') + .attr('stroke-width', 3) + .attr('paint-order', 'stroke') + .attr('font-size', '13px') + .style('pointer-events', 'none'); + + return g; + } ); - - antonyms. forEach((wordData, index) => { - const node = createWordNode(wordData, index, antonymPositions[index], currentX, currentY); - nextWordNodes.push(node); - }); - // Related words spawn in left section (180° centered, 100° range) - const relatedPositions = distributeNodesInSection( - related.length, - 180, - 100, - baseRadius - ); - - related.forEach((wordData, index) => { - const node = createWordNode(wordData, index, relatedPositions[index], currentX, currentY); - nextWordNodes.push(node); - }); + // Update circle attributes + node.select('circle') + .attr('r', d => d.group === 'path' || d.group === 'target' ? 20 : 15) + .attr('fill', d => colorScale(d.group, d.type)); - // Other words - const otherPositions = distributeNodesInSection( - other.length, - -135, - 80, - baseRadius - ); + // Update text attributes + node.select('text') + .text(d => d.id) + .attr('font-weight', d => d.group === 'path' || d.group === 'target' ? 'bold' : 'normal'); + + // Remove old event listeners and add new ones + node.on('click', null).on('mouseenter', null).on('mouseleave', null); + + // Click handler for nodes + node.on('click', (event, d) => { + event.stopPropagation(); - other.forEach((wordData, index) => { - const node = createWordNode(wordData, index, otherPositions[index], currentX, currentY); - nextWordNodes. push(node); - }); - } + if (d.group === 'option') { + onSelectWord(d.id); + } else if (d.group === 'path' && d.pathIndex !== undefined && d.pathIndex < path.length - 1) { + onRevertToWord(d.id, d.pathIndex); + } + }); - // Collect all occupied positions (path + current options) - const allOccupiedPositions: Array<{ x: number; y: number }> = [ - ...pathNodes.map(node => node.position), - ...nextWordNodes.map(node => node.position), - ]; + // Highlight on hover + node.on('mouseenter', function(event, d) { + if (d.group === 'option' || (d.group === 'path' && d.pathIndex !== undefined && d.pathIndex < path.length - 1)) { + const baseRadius = d.group === 'path' ? 20 : 15; + d3.select(this).select('circle') + .transition() + .duration(200) + .attr('r', baseRadius + 4); + } + }); - // Add historical nodes that are not in current path or current options - const pathWordSet = new Set(path.map(w => w.toLowerCase())); - const historicalNodes: Node[] = []; - - // Get the current word's position for calculating adjustments - const currentNodePosition = wordPositionsRef.current.get(currentWord.toLowerCase()) || - { x: (path.length - 1) * 200, y: 100 }; - - historicalNodesRef.current.forEach((historicalNode, wordKey) => { - // Skip if this word is in the current path or current word options - if (! pathWordSet.has(wordKey) && !currentWordSet.has(wordKey)) { - // Check if the historical node's stored position would collide - let finalPosition = historicalNode.position; - - if (checkCollision(historicalNode.position, allOccupiedPositions)) { - // Find a non-colliding position near the original - finalPosition = findNonCollidingPosition( - historicalNode. position, - currentNodePosition. x, - currentNodePosition. y, - allOccupiedPositions, - undefined // Let it calculate angle from position - ); - - // Update the stored position - historicalNodesRef.current.set(wordKey, { - ...historicalNode, - position: finalPosition, - }); - } - - // Add to occupied positions so subsequent historical nodes avoid it - allOccupiedPositions.push(finalPosition); - - historicalNodes.push({ - id: `historical-${historicalNode.word}`, - type: 'custom', - data: { - label: historicalNode. word, - backgroundColor: '#9ca3af', // gray - color: '#374151', - borderColor: '#6b7280', - definition: historicalNode.definition || 'Previously shown option', - isHistorical: true, - }, - position: finalPosition, - draggable: false, - }); + node.on('mouseleave', function(event, d) { + if (d.group === 'option' || (d.group === 'path' && d.pathIndex !== undefined && d.pathIndex < path.length - 1)) { + const baseRadius = d.group === 'path' ? 20 : 15; + d3.select(this).select('circle') + .transition() + .duration(200) + .attr('r', baseRadius); } }); - setNodes([...pathNodes, ...nextWordNodes, ...historicalNodes]); + // Update positions on tick + function ticked() { + link + .attr('x1', d => (d.source as GraphNode).x!) + .attr('y1', d => (d.source as GraphNode).y!) + .attr('x2', d => (d.target as GraphNode).x!) + .attr('y2', d => (d.target as GraphNode).y!); - // Create edges connecting all selected path nodes - const pathEdges: Edge[] = []; - for (let i = 0; i < path.length - 1; i++) { - pathEdges.push({ - id: `path-edge-${i}`, - source: `path-${path[i]}-${i}`, - target: `path-${path[i + 1]}-${i + 1}`, - animated: i === path.length - 2, // Animate the most recent connection - style: { stroke: '#8b5cf6', strokeWidth: 3 }, - type: 'smoothstep', - }); + node.attr('transform', d => `translate(${d.x},${d.y})`); } - // Create edges from current word to all next word options (not historical) - const currentNodeId = `path-${currentWord}-${path.length - 1}`; - const currentNodeExists = pathNodes.some(node => node.id === currentNodeId); - - const nextEdges: Edge[] = ! isLoading && nextWordNodes.length > 0 && currentNodeExists - ? nextWordNodes.map((node, index) => ({ - id: `next-edge-${index}`, - source: currentNodeId, - target: node.id, - animated: false, - style: { stroke: '#cbd5e1', strokeWidth: 2, strokeDasharray: '5,5' }, - type: 'smoothstep', - })) - : []; - - // Create edges from current word to historical nodes (very faint) - const historicalEdges: Edge[] = ! isLoading && historicalNodes. length > 0 && currentNodeExists - ? historicalNodes. map((node, index) => ({ - id: `historical-edge-${index}`, - source: currentNodeId, - target: node.id, - animated: false, - style: { stroke: '#e5e7eb', strokeWidth: 1, strokeDasharray: '5,5' }, - type: 'smoothstep', - })) - : []; - - setEdges([...pathEdges, ...nextEdges, ...historicalEdges]); - - // Debug logging - console.log('Path Nodes:', pathNodes.map(n => n.id)); - console.log('Next Nodes:', nextWordNodes.map(n => n.id)); - console.log('Historical Nodes:', historicalNodes.map(n => n.id)); - console.log('Path Edges:', pathEdges); - console.log('Next Edges:', nextEdges); - console.log('Historical Edges:', historicalEdges); - }, [path, currentWord, targetWord, words, isLoading, setNodes, setEdges]); - - const handleNodeClick: NodeMouseHandler = (_event, node) => { - // Handle path node clicks (revert) - if (node.data.isPathNode && typeof node.data.pathIndex === 'number') { - const index = node.data.pathIndex; - const word = path[index]; - if (index < path.length - 1) { - onRevertToWord(word, index); - } + simulation.on('tick', ticked); + + // Drag functions + function dragstarted(event: d3.D3DragEvent) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + event.subject.fx = event.subject.x; + event.subject.fy = event.subject.y; } - - // Handle next word option clicks (including historical) - if (node.id. startsWith('next-') || node.id.startsWith('historical-')) { - const word = node.data.label; - onSelectWord(word); + + function dragged(event: d3.D3DragEvent) { + event.subject.fx = event.x; + event.subject.fy = event.y; } - }; - - // Thermometer colors based on proximity - const getThermometerColor = () => { - if (proximity >= 80) return '#ef4444'; // hot red - if (proximity >= 60) return '#f97316'; // orange - if (proximity >= 40) return '#eab308'; // yellow - if (proximity >= 20) return '#3b82f6'; // blue - return '#06b6d4'; // cold cyan - }; - - const getThermometerLabel = () => { - if (proximity >= 80) return 'Very Hot! 🔥'; - if (proximity >= 60) return 'Hot 🌡️'; - if (proximity >= 40) return 'Warm ☀️'; - if (proximity >= 20) return 'Cool ❄️'; - return 'Cold 🧊'; - }; + + function dragended(event: d3.D3DragEvent) { + if (!event.active) simulation.alphaTarget(0); + event.subject.fx = null; + event.subject.fy = null; + } + }, [path, currentWord, targetWord, words, onSelectWord, onRevertToWord]); + + // State for tooltip + const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string; visible: boolean }>({ + x: 0, + y: 0, + text: '', + visible: false, + }); + + // Add hover handlers after the graph is created + useEffect(() => { + if (!svgRef.current) return; + + const svg = d3.select(svgRef.current); + + svg.selectAll('g.node-group').on('mouseenter', async function(event, d: any) { + const node = d as GraphNode; + const definition = await fetchDefinition(node.id); + + setTooltip({ + x: event.pageX, + y: event.pageY - 10, + text: `${node.id}: ${definition}`, + visible: true, + }); + }).on('mousemove', function(event) { + setTooltip(prev => ({ + ...prev, + x: event.pageX, + y: event.pageY - 10, + })); + }).on('mouseleave', function() { + setTooltip(prev => ({ ...prev, visible: false })); + }); + }, [path, words]); return ( -
      - - - - { - if (node.id.startsWith('path-')) return '#a855f7'; - if (node.id.startsWith('historical-')) return '#9ca3af'; - return '#cbd5e1'; - }} - maskColor="rgba(0, 0, 0, 0.1)" - /> - - {/* Legend Panel */} - -

      Legend

      -
      -
      +
      + {/* Legend */} +
      +

      Legend

      +
      +
      +
      + Path +
      +
      +
      + Target +
      +
      +
      Synonym
      -
      -
      +
      +
      Antonym
      -
      -
      +
      +
      Related
      -
      -
      - Visited -
      -
      -
      - Old Options -
      -
      -
      - Path -
      -
      -
      - Options -
      - - - {/* Thermometer Panel */} - -
      -

      Proximity

      -
      - {getThermometerLabel()} -
      -
      -
      -
      -
      {proximity}%
      -
      -
      - - {/* Instructions Panel */} - -

      How to play:

      -
        -
      • • Click a word option to move forward
      • -
      • • Click a visited word to go back
      • -
      • • Greyed out words are old options
      • -
      • • Drag to pan, scroll to zoom
      • -
      • • Hover over words for definitions
      • -
      -
      - - {isLoading && ( - -
      -
      - Loading words... -
      -
      - )} - +
      +
      + + {/* Proximity indicator */} +
      +

      Proximity to Target

      +
      +
      +
      +

      {proximity}%

      +
      + + {/* Loading indicator */} + {isLoading && ( +
      +
      +
      + )} + + {/* Tooltip */} + {tooltip.visible && ( +
      + {tooltip.text} +
      + )} + + {/* SVG Canvas */} +
      ); -} \ No newline at end of file +} diff --git a/client/src/hooks/useGame.ts b/client/src/hooks/useGame.ts index 846d642..c6bd513 100644 --- a/client/src/hooks/useGame.ts +++ b/client/src/hooks/useGame.ts @@ -29,7 +29,7 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { const calculateProximity = useCallback(async (word: string): Promise => { try { - const response = await fetch('/api/dist', { + const response = await fetch('/api/similarity', { headers: { 'X-Game-Id': gameId, 'X-Current-Word': word, @@ -37,21 +37,19 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { }); if (!response.ok) { + console.error('Similarity API error:', response.status, await response.text()); return 50; } const data = await response.json(); + console.log('Similarity data:', data); - if (!data.reachable) { - return 0; - } - - // Convert distance to proximity (lower distance = higher proximity) - // Distance of 0 = 100%, distance of 10+ = 0% - const maxDistance = 10; - const normalizedDistance = Math.min(data.distance || maxDistance, maxDistance); - return Math.round(100 - (normalizedDistance / maxDistance) * 100); - } catch { + // Convert similarity (0-1 range) to percentage (0-100) + // Similarity uses cosine similarity where 1 = identical, 0 = unrelated + const similarity = data.similarity || 0; + return Math.round(Math.max(0, Math.min(100, similarity * 100))); + } catch (error) { + console.error('Failed to calculate proximity:', error); return 50; } }, [gameId]); diff --git a/server/main.py b/server/main.py index 15d4506..ba4d84e 100644 --- a/server/main.py +++ b/server/main.py @@ -10,11 +10,13 @@ def create_app(): from routes.start import start_bp from routes.dist import dist_bp from routes.next import next_bp + from routes.similarity import similarity_bp app.register_blueprint(health_bp) app.register_blueprint(start_bp, url_prefix="/api") app.register_blueprint(dist_bp, url_prefix="/api") app.register_blueprint(next_bp, url_prefix="/api") + app.register_blueprint(similarity_bp, url_prefix="/api") return app diff --git a/server/routes/similarity.py b/server/routes/similarity.py index 9c29652..2924b59 100644 --- a/server/routes/similarity.py +++ b/server/routes/similarity.py @@ -34,5 +34,5 @@ def similarity(): ) return jsonify( - {"currentWord": node.word, "endWord": game.end.word, "similarity": sim} + {"currentWord": node.word, "endWord": game.end.word, "similarity": float(sim)} ) diff --git a/server/services/game.py b/server/services/game.py index bb825ed..2ae32ca 100644 --- a/server/services/game.py +++ b/server/services/game.py @@ -10,7 +10,6 @@ model = SentenceTransformer("all-MiniLM-L6-v2") - class Node: def __init__(self, word: str): self.word = word From 9fe2946be5f6142d672ebdf0575b5fb80b22074f Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sun, 18 Jan 2026 08:23:18 +0800 Subject: [PATCH 18/21] update shortest path log --- client/package-lock.json | 596 +++++++++++++++------------ client/src/components/GameScreen.tsx | 8 +- client/src/hooks/useGame.ts | 54 ++- server/routes/start.py | 5 + server/services/game.py | 49 ++- 5 files changed, 415 insertions(+), 297 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 6dded1d..015663d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -335,7 +335,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -352,7 +351,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -369,7 +367,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -386,7 +383,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -403,7 +399,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -420,7 +415,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -437,7 +431,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -454,7 +447,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -471,7 +463,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -488,7 +479,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -505,7 +495,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -522,7 +511,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -539,7 +527,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -556,7 +543,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -573,7 +559,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -590,7 +575,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -607,7 +591,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -624,7 +607,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -641,7 +623,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -658,7 +639,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -675,7 +655,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -692,7 +671,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -709,7 +687,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -726,7 +703,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -743,7 +719,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -760,7 +735,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -983,7 +957,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -994,7 +967,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1005,7 +977,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1015,14 +986,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1067,108 +1036,6 @@ "node": ">= 8" } }, - "node_modules/@reactflow/background": { - "version": "11.3.14", - "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", - "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/controls": { - "version": "11.2.14", - "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", - "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/core": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", - "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", - "license": "MIT", - "dependencies": { - "@types/d3": "^7.4.0", - "@types/d3-drag": "^3.0.1", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/minimap": { - "version": "11.7.14", - "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", - "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-resizer": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", - "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.4", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@reactflow/node-toolbar": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", - "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", - "license": "MIT", - "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -1183,7 +1050,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1197,7 +1063,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1211,7 +1076,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1225,7 +1089,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1239,7 +1102,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1253,7 +1115,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1267,7 +1128,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1281,7 +1141,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1295,7 +1154,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1309,7 +1167,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1323,7 +1180,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1337,7 +1193,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1351,7 +1206,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1365,7 +1219,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1379,7 +1232,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1393,7 +1245,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1407,7 +1258,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1421,7 +1271,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1435,7 +1284,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1449,7 +1297,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1463,7 +1310,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1477,7 +1323,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1491,7 +1336,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1505,7 +1349,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1519,13 +1362,281 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1828,7 +1939,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/geojson": { @@ -1848,7 +1958,7 @@ "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2489,12 +2599,6 @@ "node": ">= 6" } }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3012,10 +3116,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", - "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -3041,11 +3142,23 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -3355,7 +3468,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -3451,7 +3563,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3508,6 +3619,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3679,10 +3796,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", - "optional": true, - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3782,10 +3896,7 @@ "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "dev": true, "license": "MPL-2.0", - "optional": true, - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -3817,13 +3928,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3839,13 +3948,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3861,13 +3968,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3883,13 +3988,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3905,13 +4008,11 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3927,13 +4028,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3949,13 +4048,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3971,13 +4068,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -3993,13 +4088,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -4015,13 +4108,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -4037,13 +4128,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -4102,6 +4191,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4175,7 +4273,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -4328,14 +4425,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4368,7 +4463,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4605,24 +4699,6 @@ "node": ">=0.10.0" } }, - "node_modules/reactflow": { - "version": "11.11.4", - "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", - "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", - "license": "MIT", - "dependencies": { - "@reactflow/background": "11.3.14", - "@reactflow/controls": "11.2.14", - "@reactflow/core": "11.11.4", - "@reactflow/minimap": "11.7.14", - "@reactflow/node-resizer": "2.2.14", - "@reactflow/node-toolbar": "1.3.14" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4690,12 +4766,6 @@ "node": ">=4" } }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4707,11 +4777,16 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -4752,18 +4827,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4788,6 +4851,18 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -4831,7 +4906,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4873,6 +4947,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4947,6 +5031,19 @@ "jiti": "bin/jiti.js" } }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -4974,7 +5071,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -5075,7 +5171,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -5119,15 +5215,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5139,7 +5226,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -5247,7 +5333,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index c119d65..c212470 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -32,7 +32,7 @@ export default function GameScreen({ startWord, targetWord, playerName, gameId, onComplete({ playerName, path: game.path, - moves: game.path.length - 1, + moves: game.clickCount, timeSeconds: timer.seconds, }); } @@ -58,15 +58,15 @@ export default function GameScreen({ startWord, targetWord, playerName, gameId,

      Moves

      -

      {game.path.length - 1}

      +

      {game.clickCount}

      {/* Main Game Area with Interactive Graph */}
      - { + fetch('/api/start', { method: 'POST' }) + .then(res => res.json()) + .then(data => { + if (data.shortestPath) { + console.log('Shortest path (solution):', data.shortestPath.join(' -> ')); + } else { + console.log('No shortest path returned from backend.'); + } + }) + .catch(err => { + console.log('Error fetching shortest path:', err); + }); + }, []); + const calculateProximity = useCallback(async (word: string): Promise => { try { const response = await fetch('/api/similarity', { @@ -43,7 +62,7 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { const data = await response.json(); console.log('Similarity data:', data); - + // Convert similarity (0-1 range) to percentage (0-100) // Similarity uses cosine similarity where 1 = identical, 0 = unrelated const similarity = data.similarity || 0; @@ -56,7 +75,7 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { const fetchWords = useCallback(async (word: string) => { setState(prev => ({ ...prev, isLoading: true })); - + try { const response = await fetch('/api/next', { headers: { @@ -70,45 +89,45 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { } const data = await response.json(); - + // Map backend response to WordWithMetadata format const synonyms: WordWithMetadata[] = (data.synonyms || []).map((word: string) => ({ word, definition: 'Synonym', type: 'synonym' as const, })); - + const antonyms: WordWithMetadata[] = (data.antonyms || []).map((word: string) => ({ word, definition: 'Antonym', type: 'antonym' as const, })); - + const related: WordWithMetadata[] = (data.related || []).map((word: string) => ({ word, definition: 'Related word', type: 'related' as const, })); - + const allWords = [...synonyms, ...antonyms, ...related]; - + // Calculate proximity to target const proximity = await calculateProximity(word); - + setState(prev => ({ ...prev, - words: allWords.length > 0 - ? allWords + words: allWords.length > 0 + ? allWords : [{ word: 'No words found', definition: '', type: 'synonym' }], isLoading: false, proximity, })); } catch (error) { console.error('Failed to fetch words:', error); - setState(prev => ({ - ...prev, + setState(prev => ({ + ...prev, words: [{ word: 'Error loading words', definition: '', type: 'synonym' }], - isLoading: false + isLoading: false })); } }, [gameId, calculateProximity]); @@ -121,15 +140,15 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { setState(prev => { const newPath = [...prev.path, word]; const isComplete = word.toLowerCase() === prev.targetWord.toLowerCase(); - return { ...prev, currentWord: word, path: newPath, isComplete, + clickCount: prev.clickCount + 1, }; }); - + if (word.toLowerCase() !== state.targetWord.toLowerCase()) { fetchWords(word); } @@ -138,15 +157,14 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { const revertToWord = useCallback((word: string, index: number) => { setState(prev => { const newPath = prev.path.slice(0, index + 1); - return { ...prev, currentWord: word, path: newPath, isComplete: false, + clickCount: prev.clickCount + 1, }; }); - fetchWords(word); }, [fetchWords]); diff --git a/server/routes/start.py b/server/routes/start.py index 685c7f4..908da90 100644 --- a/server/routes/start.py +++ b/server/routes/start.py @@ -16,11 +16,16 @@ def start_game(): GameManager.save_game(game_id, game) + # Return both the puzzle path (random walk) and the true shortest path (solution) + puzzle_path_words = [node.word for node in getattr(game, "puzzle_path", [])] + solution_path_words = [node.word for node in getattr(game, "solution_path", [])] return jsonify( { "gameId": game_id, "startWord": game.start.word, "targetWord": game.end.word, "optimalDistance": game.shortest_path(game.start), + "puzzlePath": puzzle_path_words, + "shortestPath": solution_path_words, } ) diff --git a/server/services/game.py b/server/services/game.py index 2ae32ca..749dcf6 100644 --- a/server/services/game.py +++ b/server/services/game.py @@ -10,6 +10,7 @@ model = SentenceTransformer("all-MiniLM-L6-v2") + class Node: def __init__(self, word: str): self.word = word @@ -60,12 +61,13 @@ class Game: def __init__(self, walk_steps=10, min_path_length=4, max_attempts=100): self.graph = Graph() + # Generate puzzle path (random walk) random_word = random.choice(list(self.graph.nodes.keys())) self.start = self.graph.nodes[random_word] - self.end, self.path = self.random_walk(self.start, walk_steps) - curr_max_length = len(self.path) + self.end, self.puzzle_path = self.random_walk(self.start, walk_steps) + curr_max_length = len(self.puzzle_path) attempts = 0 - while len(self.path) < min_path_length and attempts < max_attempts: + while len(self.puzzle_path) < min_path_length and attempts < max_attempts: random_word = random.choice(list(self.graph.nodes.keys())) start = self.graph.nodes[random_word] end, path = self.random_walk(start, walk_steps) @@ -73,13 +75,19 @@ def __init__(self, walk_steps=10, min_path_length=4, max_attempts=100): curr_max_length = len(path) self.start = start self.end = end - self.path = path + self.puzzle_path = path attempts += 1 + # Compute and store the true shortest path (solution) self.visited: set[Node] = set() self.shortest_path_cache: dict[tuple[Node, Node], int] = {} self.queue = deque([(self.end, 0, [self.end])]) # curr, dist, path self.shortest_path(self.start) + self.solution_path = list( + getattr(self, "path", []) + ) # store the shortest path found + print("Puzzle path:", " -> ".join([node.word for node in self.puzzle_path])) + print("Shortest path:", " -> ".join([node.word for node in self.solution_path])) def random_walk(self, start: Node, steps: int) -> tuple[Node, list[Node]]: path = [start] @@ -105,23 +113,23 @@ def random_walk(self, start: Node, steps: int) -> tuple[Node, list[Node]]: curr = next_node return curr, path - def shortest_path(self, end: Node) -> int: - if (self.end, end) in self.shortest_path_cache: - return self.shortest_path_cache[(self.end, end)] - while self.queue: - curr, dist, path = self.queue.popleft() - self.shortest_path_cache[(self.end, curr)] = dist - for neighbor in curr.synonyms | curr.antonyms | curr.related: - if neighbor not in self.visited: - self.visited.add(neighbor) - self.queue.append((neighbor, dist + 1, path + [neighbor])) + def shortest_path(self, start: Node, end: Node = None) -> list: + # Standard BFS from start to end, returns the path as a list of nodes + if end is None: + end = self.end + from collections import deque + visited = set() + queue = deque([(start, [start])]) + while queue: + curr, path = queue.popleft() if curr == end: - if end == self.start: # only for intial call - self.path = path - return dist - - return -1 + return path + visited.add(curr) + for neighbor in curr.synonyms | curr.antonyms | curr.related: + if neighbor not in visited: + queue.append((neighbor, path + [neighbor])) + return [] def similarity(self, node: Node) -> float: embeddings = model.encode([node.word, self.end.word]) @@ -148,7 +156,8 @@ def _play(self): # self.path = path print(f"Start word: {self.start.word}, End word: {self.end.word}") - print("Possible Path:", " -> ".join([node.word for node in self.path])) + solution = self.shortest_path(self.start, self.end) + print("Shortest Path:", " -> ".join([node.word for node in solution])) curr = self.start num_actions = 0 From 431afd8870a97b847804b4312fd8a52994406faf Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sun, 18 Jan 2026 08:51:49 +0800 Subject: [PATCH 19/21] fix shortest path log --- client/src/components/GameScreen.tsx | 13 ++++- client/src/components/StartScreen.tsx | 79 ++++++++++++++++----------- client/src/hooks/useGame.ts | 49 +++++++++++------ server/routes/start.py | 8 ++- server/services/game.py | 20 +------ 5 files changed, 97 insertions(+), 72 deletions(-) diff --git a/client/src/components/GameScreen.tsx b/client/src/components/GameScreen.tsx index c212470..f7b0e95 100644 --- a/client/src/components/GameScreen.tsx +++ b/client/src/components/GameScreen.tsx @@ -15,17 +15,24 @@ interface Props { targetWord: string; playerName: string; gameId: string; + shortestPath?: string[]; + shortestPathString?: string; + optimalDistance?: number; onComplete: (result: GameResult) => void; } -export default function GameScreen({ startWord, targetWord, playerName, gameId, onComplete }: Props) { - const game = useGame(startWord, targetWord, gameId); +export default function GameScreen({ startWord, targetWord, playerName, gameId, shortestPath, shortestPathString, optimalDistance, onComplete }: Props) { + const game = useGame(startWord, targetWord, gameId, shortestPath, shortestPathString, optimalDistance); const timer = useTimer(!game.isComplete); useEffect(() => { game.fetchWords(startWord); + if (shortestPath && shortestPathString) { + console.log('Shortest path length:', shortestPath.length - 1); + console.log('Shortest path (solution):', shortestPathString); + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [shortestPath, shortestPathString]); useEffect(() => { if (game.isComplete) { diff --git a/client/src/components/StartScreen.tsx b/client/src/components/StartScreen.tsx index e87ecf7..9fff0e8 100644 --- a/client/src/components/StartScreen.tsx +++ b/client/src/components/StartScreen.tsx @@ -1,7 +1,15 @@ import { useState } from 'react'; interface Props { - onStart: (config: { startWord: string; targetWord: string; playerName: string; gameId: string }) => void; + onStart: (config: { + startWord: string; + targetWord: string; + playerName: string; + gameId: string; + shortestPath: string[]; + shortestPathString: string; + optimalDistance: number; + }) => void; } export default function StartScreen({ onStart }: Props) { @@ -16,6 +24,7 @@ export default function StartScreen({ onStart }: Props) { setIsLoading(true); try { + console.log('[StartScreen] Calling /api/start...'); const response = await fetch('/api/start', { method: 'POST', }); @@ -25,13 +34,17 @@ export default function StartScreen({ onStart }: Props) { } const data = await response.json(); - + console.log('[StartScreen] /api/start response:', data); onStart({ startWord: data.startWord, targetWord: data.targetWord, playerName: name, gameId: data.gameId, + shortestPath: data.shortestPath, + shortestPathString: data.shortestPathString, + optimalDistance: data.optimalDistance, }); + } catch (error) { console.error('Error starting game:', error); alert('Failed to start game. Please try again.'); @@ -42,37 +55,37 @@ export default function StartScreen({ onStart }: Props) { return (
      -
      - {/* Logo Icon */} -
      -
      - {/* Connected nodes logo */} - - {/* Connection lines */} - - - - - {/* Nodes */} - - - - - - - - - - {/* Sparkle effect */} - - - - - -
      -
      - -

      +
      + {/* Logo Icon */} +
      +
      + {/* Connected nodes logo */} + + {/* Connection lines */} + + + + + {/* Nodes */} + + + + + + + + + + {/* Sparkle effect */} + + + + + +
      +
      + +

      SYNCITY

      diff --git a/client/src/hooks/useGame.ts b/client/src/hooks/useGame.ts index 5ea1730..b93144a 100644 --- a/client/src/hooks/useGame.ts +++ b/client/src/hooks/useGame.ts @@ -15,9 +15,36 @@ interface GameState { isComplete: boolean; proximity: number; // 0-100, higher = closer to target clickCount: number; // total node clicks + shortestPath?: string[]; + shortestPathString?: string; + optimalDistance?: number; } -export function useGame(startWord: string, targetWord: string, gameId: string) { +export function useGame( + startWord: string, + targetWord: string, + gameId: string, + shortestPath?: string[], + shortestPathString?: string, + optimalDistance?: number +) { + // If shortestPath is not provided but shortestPathString is, split and trim it + let derivedShortestPath: string[] | undefined = shortestPath; + if ((!shortestPath || shortestPath.length === 0) && shortestPathString) { + derivedShortestPath = shortestPathString.split('->').map(w => w.trim()).filter(Boolean); + } + // If optimalDistance is not provided, derive from path + let derivedOptimalDistance = optimalDistance; + if (derivedOptimalDistance === undefined && derivedShortestPath) { + derivedOptimalDistance = derivedShortestPath.length > 0 ? derivedShortestPath.length - 1 : -1; + } + + // Log the shortest path, string, and optimal distance at first instance + if (derivedShortestPath || shortestPathString || derivedOptimalDistance !== undefined) { + console.log('[useGame] Shortest path:', derivedShortestPath); + console.log('[useGame] Shortest path string:', shortestPathString); + console.log('[useGame] Optimal distance:', derivedOptimalDistance); + } const [state, setState] = useState({ currentWord: startWord, targetWord, @@ -27,24 +54,12 @@ export function useGame(startWord: string, targetWord: string, gameId: string) { isComplete: false, proximity: 0, clickCount: 0, + shortestPath: derivedShortestPath, + shortestPathString, + optimalDistance: derivedOptimalDistance, }); - // Fetch and log the shortest path for debug purposes - // Only runs on mount/initialization - useEffect(() => { - fetch('/api/start', { method: 'POST' }) - .then(res => res.json()) - .then(data => { - if (data.shortestPath) { - console.log('Shortest path (solution):', data.shortestPath.join(' -> ')); - } else { - console.log('No shortest path returned from backend.'); - } - }) - .catch(err => { - console.log('Error fetching shortest path:', err); - }); - }, []); + // No duplicate /api/start call. Shortest path is passed in props/state. const calculateProximity = useCallback(async (word: string): Promise => { try { diff --git a/server/routes/start.py b/server/routes/start.py index 908da90..59511f9 100644 --- a/server/routes/start.py +++ b/server/routes/start.py @@ -19,13 +19,19 @@ def start_game(): # Return both the puzzle path (random walk) and the true shortest path (solution) puzzle_path_words = [node.word for node in getattr(game, "puzzle_path", [])] solution_path_words = [node.word for node in getattr(game, "solution_path", [])] + # Compute the shortest path as a list of nodes + shortest_path_nodes = game.shortest_path(game.start, game.end) + solution_path_words = [node.word for node in shortest_path_nodes] + optimal_distance = len(shortest_path_nodes) - 1 if shortest_path_nodes else -1 + shortest_path_string = " -> ".join(solution_path_words) return jsonify( { "gameId": game_id, "startWord": game.start.word, "targetWord": game.end.word, - "optimalDistance": game.shortest_path(game.start), + "optimalDistance": optimal_distance, "puzzlePath": puzzle_path_words, "shortestPath": solution_path_words, + "shortestPathString": shortest_path_string, } ) diff --git a/server/services/game.py b/server/services/game.py index 749dcf6..aba0d4d 100644 --- a/server/services/game.py +++ b/server/services/game.py @@ -113,12 +113,12 @@ def random_walk(self, start: Node, steps: int) -> tuple[Node, list[Node]]: curr = next_node return curr, path - - def shortest_path(self, start: Node, end: Node = None) -> list: + def shortest_path(self, start: Node, end: "Node | None" = None) -> list: # Standard BFS from start to end, returns the path as a list of nodes if end is None: end = self.end from collections import deque + visited = set() queue = deque([(start, [start])]) while queue: @@ -139,22 +139,6 @@ def similarity(self, node: Node) -> float: return sim def _play(self): - # if start_word in self.graph.nodes: - # start = self.graph.nodes[start_word] - # else: - # random_word = random.choice(list(self.graph.nodes.keys())) - # start = self.graph.nodes[random_word] - - # end, path = self.random_walk(start, steps) - # while len(path) < min_path_length: - # random_word = random.choice(list(self.graph.nodes.keys())) - # start = self.graph.nodes[random_word] - # end, path = self.random_walk(start, steps) - - # self.start = start - # self.end = end - # self.path = path - print(f"Start word: {self.start.word}, End word: {self.end.word}") solution = self.shortest_path(self.start, self.end) print("Shortest Path:", " -> ".join([node.word for node in solution])) From e8c003e66bcd545b3f11bb2002104fd77ddf965b Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sun, 18 Jan 2026 09:20:45 +0800 Subject: [PATCH 20/21] added quit screen --- .python-version | 1 - client/src/App.tsx | 18 ++++- client/src/components/EndScreen.tsx | 100 +++++++++++++++----------- client/src/components/GameScreen.tsx | 29 ++++++++ client/src/components/QuitScreen.tsx | 84 ++++++++++++++++++++++ client/src/components/StartScreen.tsx | 6 +- 6 files changed, 191 insertions(+), 47 deletions(-) delete mode 100644 .python-version create mode 100644 client/src/components/QuitScreen.tsx diff --git a/.python-version b/.python-version deleted file mode 100644 index 24ee5b1..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/client/src/App.tsx b/client/src/App.tsx index 9a1ac7d..65ca1a9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,15 +2,19 @@ import { useState } from 'react'; import StartScreen from './components/StartScreen'; import GameScreen from './components/GameScreen'; import EndScreen from './components/EndScreen'; +import QuitScreen from './components/QuitScreen'; import './App.css'; -type Screen = 'start' | 'game' | 'end'; +type Screen = 'start' | 'game' | 'end' | 'quit'; interface GameConfig { startWord: string; targetWord: string; playerName: string; gameId: string; + shortestPath?: string[]; + shortestPathString?: string; + optimalDistance?: number; } function App() { @@ -25,7 +29,11 @@ function App() { const endGame = (result: any) => { setGameResult(result); - setScreen('end'); + if (result.quit) { + setScreen('quit'); + } else { + setScreen('end'); + } }; const restart = () => { @@ -43,12 +51,18 @@ function App() { targetWord={gameConfig.targetWord} playerName={gameConfig.playerName} gameId={gameConfig.gameId} + shortestPath={gameConfig.shortestPath} + shortestPathString={gameConfig.shortestPathString} + optimalDistance={gameConfig.optimalDistance} onComplete={endGame} /> )} {screen === 'end' && gameResult && ( )} + {screen === 'quit' && gameResult && ( + + )}

      ); } diff --git a/client/src/components/EndScreen.tsx b/client/src/components/EndScreen.tsx index a25b4a1..4233779 100644 --- a/client/src/components/EndScreen.tsx +++ b/client/src/components/EndScreen.tsx @@ -4,6 +4,10 @@ interface Props { path: string[]; moves: number; timeSeconds: number; + proximity?: number; + shortestPathString?: string; + optimalDistance?: number; + quit?: boolean; }; onRestart: () => void; } @@ -13,48 +17,54 @@ export default function EndScreen({ result, onRestart }: Props) { const seconds = result.timeSeconds % 60; return ( -
      - {/* Floating celebration particles */} -
      -
      -
      -
      - -
      -
      -
      -
      🏆
      -
      -
      -
      -

      Congratulations!

      +
      + {/* Floating celebration particles */} +
      +
      +
      +
      + +
      + {!result.quit && ( +
      +
      +
      🏆
      +
      +
      +
      + )} +

      + {result.quit ? 'Game Over' : 'Congratulations!'} +

      {result.playerName}

      -

      You reached the target word!

      +

      + {result.quit ? 'You quit the game.' : 'You reached the target word!'} +

      -
      -
      -
      -
      - - - -
      -

      Time

      -

      - {minutes}:{seconds.toString().padStart(2, '0')} -

      -
      -
      -
      - - - -
      -

      Moves

      -

      {result.moves}

      -
      -
      -
      +
      +
      +
      +
      + + + +
      +

      Time

      +

      + {minutes}:{seconds.toString().padStart(2, '0')} +

      +
      +
      +
      + + + +
      +

      Moves

      +

      {result.moves}

      +
      +
      +

      Your path:

      @@ -69,9 +79,17 @@ export default function EndScreen({ result, onRestart }: Props) {
      ))}
      +
      + Optimal path: {result.shortestPathString || 'N/A'} +
      +
      + {result.optimalDistance !== undefined && ( +

      Optimal distance: {result.optimalDistance}

      + )} +
      -
      + {/* Quit Button */} +
      + +
      {/* Main Game Area with Interactive Graph */} diff --git a/client/src/components/QuitScreen.tsx b/client/src/components/QuitScreen.tsx new file mode 100644 index 0000000..9a7d079 --- /dev/null +++ b/client/src/components/QuitScreen.tsx @@ -0,0 +1,84 @@ +interface Props { + result: { + playerName: string; + path: string[]; + moves: number; + timeSeconds: number; + proximity?: number; + shortestPathString?: string; + optimalDistance?: number; + quit?: boolean; + }; + onRestart: () => void; +} + +export default function QuitScreen({ result, onRestart }: Props) { + const minutes = Math.floor(result.timeSeconds / 60); + const seconds = result.timeSeconds % 60; + return ( +
      +
      +

      Game Over

      +

      {result.playerName}

      +

      You quit the game.

      +
      +
      +
      +
      + + + +
      +

      Time

      +

      + {minutes}:{seconds.toString().padStart(2, '0')} +

      +
      +
      +
      + + + +
      +

      Moves

      +

      {result.moves}

      +
      +
      +
      +
      +

      Your path:

      +
      + {result.path.map((word, index) => ( +
      + + {word} + + {index < result.path.length - 1 && ( + + )} +
      + ))} +
      + {/* Extra stats for quit/gameover */} +
      + {result.shortestPathString && ( +

      Optimal path: {result.shortestPathString}

      + )} + {result.optimalDistance !== undefined && ( +

      Optimal distance: {result.optimalDistance}

      + )} +
      +
      + +
      +
      + ); +} \ No newline at end of file diff --git a/client/src/components/StartScreen.tsx b/client/src/components/StartScreen.tsx index 9fff0e8..9a0a165 100644 --- a/client/src/components/StartScreen.tsx +++ b/client/src/components/StartScreen.tsx @@ -6,9 +6,9 @@ interface Props { targetWord: string; playerName: string; gameId: string; - shortestPath: string[]; - shortestPathString: string; - optimalDistance: number; + shortestPath?: string[]; + shortestPathString?: string; + optimalDistance?: number; }) => void; } From 45ffc9d7d04c52424ec93046fe7275ab058a441a Mon Sep 17 00:00:00 2001 From: ys_teng Date: Sun, 18 Jan 2026 09:56:53 +0800 Subject: [PATCH 21/21] add leaderboard --- client/src/App.tsx | 52 +++++++++++++++++-- client/src/components/EndScreen.tsx | 33 +++++++++---- client/src/components/LeaderboardScreen.tsx | 55 +++++++++++++++++++++ server/main.py | 2 + server/routes/leaderboard.py | 47 ++++++++++++++++++ 5 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 client/src/components/LeaderboardScreen.tsx create mode 100644 server/routes/leaderboard.py diff --git a/client/src/App.tsx b/client/src/App.tsx index 65ca1a9..9435088 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,11 +1,12 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import StartScreen from './components/StartScreen'; import GameScreen from './components/GameScreen'; import EndScreen from './components/EndScreen'; import QuitScreen from './components/QuitScreen'; +import LeaderboardScreen from './components/LeaderboardScreen'; import './App.css'; -type Screen = 'start' | 'game' | 'end' | 'quit'; +type Screen = 'start' | 'game' | 'end' | 'quit' | 'leaderboard'; interface GameConfig { startWord: string; @@ -17,24 +18,64 @@ interface GameConfig { optimalDistance?: number; } +interface LeaderboardEntry { + playerName: string; + timeSeconds: number; + moves: number; + optimalDistance: number; + score: number; +} + function App() { const [screen, setScreen] = useState('start'); const [gameConfig, setGameConfig] = useState(null); const [gameResult, setGameResult] = useState(null); + const [leaderboard, setLeaderboard] = useState([]); + const [leaderboardLoaded, setLeaderboardLoaded] = useState(false); const startGame = (config: GameConfig) => { setGameConfig(config); setScreen('game'); }; - const endGame = (result: any) => { + const endGame = async (result: any) => { setGameResult(result); if (result.quit) { setScreen('quit'); } else { + // Record leaderboard entry in Redis + if (result.playerName && result.timeSeconds !== undefined && result.moves !== undefined && result.optimalDistance) { + try { + await fetch('/api/leaderboard', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + playerName: result.playerName, + timeSeconds: result.timeSeconds, + moves: result.moves, + optimalDistance: result.optimalDistance, + }), + }); + } catch (e) { + // Optionally handle error + } + } setScreen('end'); } }; + const showLeaderboard = useCallback(async () => { + try { + const res = await fetch('/api/leaderboard'); + if (res.ok) { + const data = await res.json(); + setLeaderboard(data); + setLeaderboardLoaded(true); + } + } catch (e) { + // Optionally handle error + } + setScreen('leaderboard'); + }, []); const restart = () => { setGameConfig(null); @@ -58,7 +99,10 @@ function App() { /> )} {screen === 'end' && gameResult && ( - + + )} + {screen === 'leaderboard' && ( + )} {screen === 'quit' && gameResult && ( diff --git a/client/src/components/EndScreen.tsx b/client/src/components/EndScreen.tsx index 4233779..822239f 100644 --- a/client/src/components/EndScreen.tsx +++ b/client/src/components/EndScreen.tsx @@ -10,9 +10,10 @@ interface Props { quit?: boolean; }; onRestart: () => void; + onShowLeaderboard?: () => void; } -export default function EndScreen({ result, onRestart }: Props) { +export default function EndScreen({ result, onRestart, onShowLeaderboard }: Props) { const minutes = Math.floor(result.timeSeconds / 60); const seconds = result.timeSeconds % 60; @@ -89,15 +90,27 @@ export default function EndScreen({ result, onRestart }: Props) {

      - +
      + + +
      ); diff --git a/client/src/components/LeaderboardScreen.tsx b/client/src/components/LeaderboardScreen.tsx new file mode 100644 index 0000000..5e6d01b --- /dev/null +++ b/client/src/components/LeaderboardScreen.tsx @@ -0,0 +1,55 @@ +interface LeaderboardEntry { + playerName: string; + timeSeconds: number; + moves: number; + optimalDistance: number; + score: number; +} + +interface Props { + leaderboard: LeaderboardEntry[]; + onRestart: () => void; +} + +export default function LeaderboardScreen({ leaderboard, onRestart }: Props) { + // Sort by score ascending (smaller is better) + const sorted = [...leaderboard].sort((a, b) => a.score - b.score); + return ( +
      +
      +

      Leaderboard

      + + + + + + + + + + + + {sorted.map((entry, i) => ( + + + + + + + + ))} + +
      RankPlayerTime (s)MovesOptimal
      {i + 1}{entry.playerName}{entry.timeSeconds}{entry.moves}{entry.optimalDistance}
      + +
      +
      + ); +} \ No newline at end of file diff --git a/server/main.py b/server/main.py index ba4d84e..3d4d0c2 100644 --- a/server/main.py +++ b/server/main.py @@ -11,12 +11,14 @@ def create_app(): from routes.dist import dist_bp from routes.next import next_bp from routes.similarity import similarity_bp + from routes.leaderboard import leaderboard_bp app.register_blueprint(health_bp) app.register_blueprint(start_bp, url_prefix="/api") app.register_blueprint(dist_bp, url_prefix="/api") app.register_blueprint(next_bp, url_prefix="/api") app.register_blueprint(similarity_bp, url_prefix="/api") + app.register_blueprint(leaderboard_bp, url_prefix="/api") return app diff --git a/server/routes/leaderboard.py b/server/routes/leaderboard.py new file mode 100644 index 0000000..088a106 --- /dev/null +++ b/server/routes/leaderboard.py @@ -0,0 +1,47 @@ +from flask import Blueprint, request, jsonify +import redis +import pickle + +leaderboard_bp = Blueprint("leaderboard", __name__) + +# Use the same Redis config as game_manager +r = redis.Redis(host="localhost", port=6379, db=0) +LEADERBOARD_KEY = "leaderboard" + + +@leaderboard_bp.route("/leaderboard", methods=["GET"]) +def get_leaderboard(): + data = r.get(LEADERBOARD_KEY) + if data: + leaderboard = pickle.loads(data) + else: + leaderboard = [] + # Sort by score (smaller is better) + leaderboard = sorted(leaderboard, key=lambda x: x["score"]) + return jsonify(leaderboard) + + +@leaderboard_bp.route("/leaderboard", methods=["POST"]) +def add_leaderboard_entry(): + entry = request.json + # Validate required fields + for field in ["playerName", "timeSeconds", "moves", "optimalDistance"]: + if field not in entry: + return jsonify({"error": f"Missing field: {field}"}), 400 + # Compute score + try: + score = float(entry["timeSeconds"]) * ( + float(entry["moves"]) / float(entry["optimalDistance"]) + ) + except Exception: + return jsonify({"error": "Invalid numeric values"}), 400 + entry["score"] = score + # Load, append, and save + data = r.get(LEADERBOARD_KEY) + if data: + leaderboard = pickle.loads(data) + else: + leaderboard = [] + leaderboard.append(entry) + r.set(LEADERBOARD_KEY, pickle.dumps(leaderboard)) + return jsonify({"success": True})