AIBRUH commited on
Commit
ca9ae00
·
verified ·
1 Parent(s): 77c4da9

Upload frontend/src/App.tsx with huggingface_hub

Browse files
Files changed (1) hide show
  1. frontend/src/App.tsx +390 -0
frontend/src/App.tsx ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { Link, useNavigate } from "react-router-dom";
3
+ import ReactFlow, { Background, Controls, MiniMap, type Edge, type EdgeTypes, type Node, type NodeTypes } from "reactflow";
4
+ import "reactflow/dist/style.css";
5
+ import AnimatedPulseEdge from "./AnimatedPulseEdge";
6
+ import LakeCanvasNode, { type LakeCanvasNodeData } from "./components/LakeCanvasNode";
7
+ import Header from "./components/Header";
8
+ import Composer from "./components/Composer";
9
+ import AdminPanel from "./components/AdminPanel";
10
+ import PlayerTab from "./components/PlayerTab";
11
+ import LeadersTab from "./components/LeadersTab";
12
+ import QueriesTab, { type QueryResult } from "./components/QueriesTab";
13
+ import EngineTab from "./components/EngineTab";
14
+ import NewsTab from "./components/NewsTab";
15
+ import MatchupsTab from "./components/MatchupsTab";
16
+ import JimmyPanel from "./components/JimmyPanel";
17
+ import BooAgentPanel from "./components/BooAgentPanel";
18
+ import RampIndex from "./components/RampIndex";
19
+ import MyBooTab from "./components/MyBooTab";
20
+ import OddsTab from "./components/OddsTab";
21
+ import LiveTicker from "./components/LiveTicker";
22
+ import { api } from "./api";
23
+ import type { ChartIndexRow, PlayerPropChart } from "./types";
24
+
25
+ const VIEW_TABS = ["player", "leaders", "matchups", "ramp", "engine", "news", "queries", "myboo", "odds"] as const;
26
+ type ViewTab = (typeof VIEW_TABS)[number];
27
+
28
+ // These three are standalone pages — clicking them navigates away from the main app.
29
+ const STANDALONE_LINKS = [
30
+ { id: "throwdown", label: "THROWDOWN THURSDAY", href: "/throwdown" },
31
+ { id: "monday", label: "MONDAY NIGHT", href: "/monday" },
32
+ { id: "sunday", label: "SUNDAY SLIPS", href: "/parlays" },
33
+ ] as const;
34
+
35
+ const edgeTypes: EdgeTypes = { animatedPulse: AnimatedPulseEdge };
36
+ const nodeTypes: NodeTypes = { canvasNode: LakeCanvasNode };
37
+
38
+ export const tabPath = (t: string) => `/${t}`;
39
+
40
+ const DEFAULT_PLAYER_ID = "00-0033280"; // Christian McCaffrey -- a real player present in every gold CSV, sane default
41
+
42
+ export default function App({ initialTab }: { initialTab?: ViewTab }) {
43
+ const [isDark, setIsDark] = useState(true);
44
+ const [leftPct, setLeftPct] = useState(40);
45
+ const [rfNodes, setRfNodes] = useState<Node<LakeCanvasNodeData>[]>([]);
46
+ const [rfEdges, setRfEdges] = useState<Edge[]>([]);
47
+ const [nextNodeCount, setNextNodeCount] = useState(2);
48
+
49
+ const [tab, setTab] = useState<ViewTab>(() => {
50
+ if (initialTab) return initialTab;
51
+ try {
52
+ const t = new URLSearchParams(window.location.search).get("tab");
53
+ if (t && (VIEW_TABS as readonly string[]).includes(t)) return t as ViewTab;
54
+ } catch { /* */ }
55
+ return "leaders";
56
+ });
57
+ const navigate = useNavigate();
58
+ useEffect(() => { if (initialTab) setTab(initialTab); }, [initialTab]);
59
+ useEffect(() => {
60
+ const p = tabPath(tab);
61
+ if (window.location.pathname !== p) navigate(p, { replace: window.location.pathname === "/" });
62
+ }, [tab, navigate]);
63
+ const [canvasOpen, setCanvasOpen] = useState(false);
64
+ const [selectedPlayerId, setSelectedPlayerId] = useState(DEFAULT_PLAYER_ID);
65
+ const [playerMarket, setPlayerMarket] = useState<string | undefined>(undefined);
66
+ const [slip, setSlip] = useState<PlayerPropChart[]>([]);
67
+ const [target, setTarget] = useState("all");
68
+ const [adminOpen, setAdminOpen] = useState(false);
69
+ const [chartIndex, setChartIndex] = useState<ChartIndexRow[]>([]);
70
+ const [logs, setLogs] = useState<{ t: string; msg: string }[]>([{ t: now(), msg: "Canvas ready. Enter a query to begin." }]);
71
+ const [results, setResults] = useState<QueryResult[]>([]);
72
+ const [cleared, setCleared] = useState(false);
73
+ const [jimmyPanelOpen, setJimmyPanelOpen] = useState(false);
74
+ const [booPanelOpen, setBooPanelOpen] = useState(false);
75
+
76
+ useEffect(() => {
77
+ document.documentElement.setAttribute("data-theme", isDark ? "dark" : "light");
78
+ }, [isDark]);
79
+
80
+ useEffect(() => {
81
+ api.index().then(setChartIndex).catch(() => setChartIndex([]));
82
+ }, []);
83
+
84
+ // Data-driven nodes — fan-in topology: both RAMPs feed Jimmy at the bottom.
85
+ // NFL RAMP top-left, CFB RAMP top-right (offset right by ~Jimmy's node width
86
+ // to spread the converging edges), Jimmy centered below both.
87
+ useEffect(() => {
88
+ api.canvasNodes().then(({ nodes }) => {
89
+ const NODE_W = 210;
90
+ const positions: Record<string, { x: number; y: number }> = {
91
+ "ramp-nfl": { x: 40, y: 60 },
92
+ "ramp-cfb": { x: 40 + NODE_W + NODE_W + 30, y: 60 },
93
+ "jimmy-the-greek": { x: 40 + NODE_W + 15, y: 390 },
94
+ // the authority: MY BOO sits right beside Jimmy, same row
95
+ "my-boo": { x: 40 + NODE_W + 15 + NODE_W + 30, y: 390 },
96
+ };
97
+
98
+ // edges: every lake node feeds into every expert node
99
+ const lakeIds = nodes.filter((n) => n.type === "lake").map((n) => n.id);
100
+ const expertIds = nodes.filter((n) => n.type === "expert").map((n) => n.id);
101
+ const fanEdges: Edge[] = [];
102
+ for (const src of lakeIds) {
103
+ for (const tgt of expertIds) {
104
+ fanEdges.push({ id: `${src}->${tgt}`, source: src, target: tgt, type: "animatedPulse" });
105
+ }
106
+ }
107
+
108
+ const built = nodes.map((n, i) => ({
109
+ id: n.id,
110
+ type: "canvasNode",
111
+ position: positions[n.id] ?? { x: 40, y: 40 + i * 340 },
112
+ data: {
113
+ type: n.type, name: n.name, sub: n.sub, chips: n.chips,
114
+ ...(n.store ? { store: n.store } : {}),
115
+ ...(n.type === "expert" ? { onSettings: () => (n.id === "my-boo" ? setBooPanelOpen(true) : setJimmyPanelOpen(true)) } : {}),
116
+ },
117
+ }));
118
+
119
+ setRfNodes(built);
120
+ setRfEdges(fanEdges);
121
+ setNextNodeCount(built.length);
122
+ });
123
+ }, []);
124
+
125
+ function log(msg: string) {
126
+ setLogs((l) => [...l, { t: now(), msg }]);
127
+ }
128
+
129
+ function addNode(type: "lake" | "expert") {
130
+ const id = `${type}-${Date.now()}`;
131
+ const newNode: Node<LakeCanvasNodeData> = {
132
+ id,
133
+ type: "canvasNode",
134
+ position: { x: 40, y: nextNodeCount * 340 + 16 },
135
+ data: type === "lake"
136
+ ? { type, name: "NEW RAMP SOURCE", sub: "Phase 1 — the lake" }
137
+ : { type, name: "NEW EXPERT", sub: "harness · not wired", chips: [] },
138
+ };
139
+ setRfNodes((prev) => {
140
+ const updated = [...prev, newNode];
141
+ if (prev.length > 0) {
142
+ setRfEdges((edges) => [...edges, { id: `${prev[prev.length - 1].id}-${id}`, source: prev[prev.length - 1].id, target: id, type: "animatedPulse" }]);
143
+ }
144
+ return updated;
145
+ });
146
+ setNextNodeCount((n) => n + 1);
147
+ log(type === "lake" ? "RAMP source node added" : "Expert node added to harness");
148
+ }
149
+
150
+ const slipIds = useMemo(() => new Set(slip.map((s) => s.playerId)), [slip]);
151
+
152
+ function addToSlip(chart: PlayerPropChart) {
153
+ setSlip((prev) => (prev.some((s) => s.playerId === chart.playerId) ? prev : [...prev, chart]));
154
+ }
155
+
156
+
157
+
158
+ const targets = useMemo(
159
+ () => [{ id: "all", label: "All nodes" }, ...rfNodes.map((n) => ({ id: n.id, label: n.data.name }))],
160
+ [rfNodes]
161
+ );
162
+
163
+ const handleSubmit = useCallback(
164
+ (text: string) => {
165
+ setCleared(false);
166
+ const n = results.length + 1;
167
+ setResults((prev) => [
168
+ {
169
+ n,
170
+ time: now(),
171
+ title: text,
172
+ subtitle: `Targeted: ${targets.find((t) => t.id === target)?.label ?? "All nodes"} — echoed, no live query routing wired yet`,
173
+ stats: [
174
+ { label: "Target", value: targets.find((t) => t.id === target)?.label ?? "All nodes" },
175
+ { label: "Result", value: "not wired — see RAMP NFL / JIMMY THE GREEK nodes" },
176
+ ],
177
+ },
178
+ ...prev,
179
+ ]);
180
+ setTab("queries");
181
+ log(`Query submitted: "${text}"`);
182
+ },
183
+ [results, target, targets]
184
+ );
185
+
186
+ function handleClear() {
187
+ setResults([]);
188
+ setCleared(true);
189
+ log("Display cleared");
190
+ }
191
+
192
+ const canvasCapable = tab !== "leaders" && tab !== "matchups" && tab !== "ramp" && tab !== "myboo" && tab !== "odds";
193
+
194
+ return (
195
+ <div style={{ display: "flex", flexDirection: "column", width: "100vw", height: "100vh", background: "var(--bp-page-bg)", color: "var(--bp-fg)" }}>
196
+ <Header isDark={isDark} onToggleTheme={() => setIsDark((v) => !v)} onToggleAdmin={() => setAdminOpen((v) => !v)} />
197
+ <LiveTicker />
198
+
199
+ <div style={{ flex: 1, display: "flex", minHeight: 0, position: "relative" }}>
200
+ {canvasCapable && !canvasOpen && (
201
+ <button onClick={() => setCanvasOpen(true)} title="Show the node canvas"
202
+ style={{ position: "absolute", left: 0, top: "40%", zIndex: 20, writingMode: "vertical-rl", padding: "14px 6px", cursor: "pointer",
203
+ border: "1px solid #6b4a1c", borderLeft: 0, borderRadius: "0 10px 10px 0", background: "var(--bp-card-bg)", color: "#d9b45a",
204
+ fontSize: 11, fontWeight: 800, letterSpacing: "0.2em" }}>
205
+ NODES &#9656;
206
+ </button>
207
+ )}
208
+ {canvasOpen && canvasCapable && (
209
+ <>
210
+ <div style={{ width: `${leftPct}%`, flex: `0 0 ${leftPct}%`, display: "flex", flexDirection: "column", minWidth: 0 }}>
211
+ <div
212
+ style={{
213
+ flex: 1,
214
+ position: "relative",
215
+ overflow: "hidden",
216
+ background:
217
+ "radial-gradient(ellipse 70% 55% at 28% 30%, var(--bp-canvas-glow-orange), transparent 70%), radial-gradient(ellipse 80% 60% at 70% 78%, var(--bp-canvas-glow-purple), transparent 70%), var(--bp-canvas-bg)",
218
+ }}
219
+ >
220
+ <button onClick={() => setCanvasOpen(false)} title="Hide the node canvas"
221
+ style={{ position: "absolute", left: 10, top: 10, zIndex: 20, height: 28, padding: "0 12px", cursor: "pointer", borderRadius: 999,
222
+ border: "1px solid #6b4a1c", background: "var(--bp-card-bg)", color: "#d9b45a", fontSize: 11, fontWeight: 800, letterSpacing: "0.12em" }}>
223
+ &#9666; HIDE NODES
224
+ </button>
225
+ <ReactFlow nodes={rfNodes} edges={rfEdges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} fitView minZoom={0.2} maxZoom={1.2}>
226
+ <Background color="#2a1e36" gap={24} />
227
+ <Controls position="bottom-right" style={{ marginBottom: 100 }} />
228
+ <MiniMap
229
+ position="bottom-right"
230
+ style={{ width: 112, height: 72, background: "#140a20", border: "1px solid #2a1e36", borderRadius: 4 }}
231
+ maskColor="rgba(11,5,18,0.6)"
232
+ nodeColor="#6a4a86"
233
+ pannable={false}
234
+ zoomable={false}
235
+ />
236
+ </ReactFlow>
237
+ </div>
238
+ </div>
239
+
240
+ <ResizeHandle onChange={setLeftPct} />
241
+ </>
242
+ )}
243
+
244
+ <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--bp-page-bg)", color: "var(--bp-fg)", position: "relative" }}>
245
+ <div style={{ height: 8, flex: "0 0 8px" }} />
246
+
247
+ <div style={{ display: "flex", gap: 6, padding: "0 20px 10px", flexWrap: "wrap", flex: "0 0 auto" }}>
248
+ {/* HOME — landing page */}
249
+ <a
250
+ href="/home/index.html"
251
+ style={{
252
+ display: "inline-flex", alignItems: "center",
253
+ height: 32, padding: "0 14px", borderRadius: 999,
254
+ border: "1px solid #c9a54e",
255
+ background: "rgba(201,165,78,0.14)",
256
+ color: "#f1dc92",
257
+ fontSize: 12, fontWeight: 800, letterSpacing: "0.1em",
258
+ textDecoration: "none", cursor: "pointer",
259
+ }}
260
+ >
261
+ HOME
262
+ </a>
263
+ {/* Standalone page links — navigate away */}
264
+ {STANDALONE_LINKS.map(({ id, label, href }) => (
265
+ <Link
266
+ key={id}
267
+ to={href}
268
+ style={{
269
+ display: "inline-flex", alignItems: "center",
270
+ height: 32, padding: "0 14px", borderRadius: 999,
271
+ border: "1px solid #c9a54e",
272
+ background: "rgba(201,165,78,0.14)",
273
+ color: "#f1dc92",
274
+ fontSize: 12, fontWeight: 800, letterSpacing: "0.1em",
275
+ textDecoration: "none", cursor: "pointer",
276
+ }}
277
+ >
278
+ {label}
279
+ </Link>
280
+ ))}
281
+ {/* In-app tabs */}
282
+ {VIEW_TABS.filter((vt) => vt !== "player").map((vt) => {
283
+ const isBoo = vt === "myboo";
284
+ const isActive = vt === tab;
285
+ return (
286
+ <button
287
+ key={vt}
288
+ onClick={() => setTab(vt)}
289
+ style={{
290
+ height: 32, padding: "0 14px", borderRadius: 999,
291
+ border: `1px solid ${isActive ? (isBoo ? "#a78bfa" : "#c9a54e") : (isBoo ? "#3a2060" : "var(--bp-border)")}`,
292
+ background: isActive
293
+ ? (isBoo ? "rgba(139,92,246,0.18)" : "rgba(201,165,78,0.14)")
294
+ : (isBoo ? "rgba(139,92,246,0.06)" : "var(--bp-card-bg)"),
295
+ color: isActive ? (isBoo ? "#c4b5fd" : "#d9b45a") : (isBoo ? "#a78bfa" : "var(--bp-fg)"),
296
+ fontSize: 12, fontWeight: isBoo ? 900 : 700, cursor: "pointer",
297
+ letterSpacing: isBoo ? "0.06em" : undefined,
298
+ }}
299
+ >
300
+ {isBoo ? "MY BOO" : vt === "odds" ? "THE ODDS" : vt.toUpperCase()}
301
+ </button>
302
+ );
303
+ })}
304
+ </div>
305
+
306
+ <div style={{ flex: 1, overflowY: "auto", padding: "4px 20px 20px", minHeight: 0 }}>
307
+ {tab === "ramp" && <RampIndex />}
308
+ {tab === "player" && (
309
+ <PlayerTab
310
+ playerId={selectedPlayerId}
311
+ initialMarket={playerMarket}
312
+ onSelectPlayer={(id) => { setPlayerMarket(undefined); setSelectedPlayerId(id); }}
313
+ onSlipAdd={addToSlip}
314
+ inSlip={slipIds.has(selectedPlayerId)}
315
+ />
316
+ )}
317
+ {tab === "leaders" && (
318
+ <LeadersTab
319
+ onOpenPlayer={(id, market) => {
320
+ setPlayerMarket(market);
321
+ setSelectedPlayerId(id);
322
+ setTab("player");
323
+ }}
324
+ />
325
+ )}
326
+ {/* parlay tab removed — THROWDOWN THURSDAY is now a standalone page at /throwdown */}
327
+ {tab === "engine" && <EngineTab />}
328
+ {tab === "news" && <NewsTab />}
329
+ {tab === "matchups" && <MatchupsTab />}
330
+ {tab === "queries" && (cleared && results.length === 0 ? <QueriesTab results={[]} /> : <QueriesTab results={results} />)}
331
+ {tab === "myboo" && <MyBooTab />}
332
+ {tab === "odds" && <OddsTab />}
333
+ </div>
334
+
335
+ <Composer
336
+ targets={targets}
337
+ target={target}
338
+ onTargetChange={setTarget}
339
+ onSubmit={handleSubmit}
340
+ onClear={handleClear}
341
+ counter={results.length}
342
+ />
343
+
344
+ {adminOpen && (
345
+ <AdminPanel
346
+ onClose={() => setAdminOpen(false)}
347
+ onAddRamp={() => addNode("lake")}
348
+ onAddExpert={() => addNode("expert")}
349
+ chartIndex={chartIndex}
350
+ logs={logs}
351
+ />
352
+ )}
353
+
354
+ {jimmyPanelOpen && (
355
+ <JimmyPanel onClose={() => setJimmyPanelOpen(false)} />
356
+ )}
357
+ {booPanelOpen && <BooAgentPanel onClose={() => setBooPanelOpen(false)} />}
358
+ </div>
359
+ </div>
360
+ </div>
361
+ );
362
+ }
363
+
364
+ function ResizeHandle({ onChange }: { onChange: (pct: number) => void }) {
365
+ function onPointerDown(e: React.PointerEvent<HTMLDivElement>) {
366
+ e.preventDefault();
367
+ const root = (e.currentTarget.parentElement as HTMLElement) ?? document.body;
368
+ function move(ev: PointerEvent) {
369
+ const rect = root.getBoundingClientRect();
370
+ const pct = Math.max(22, Math.min(65, ((ev.clientX - rect.left) / rect.width) * 100));
371
+ onChange(pct);
372
+ }
373
+ function up() {
374
+ window.removeEventListener("pointermove", move);
375
+ window.removeEventListener("pointerup", up);
376
+ }
377
+ window.addEventListener("pointermove", move);
378
+ window.addEventListener("pointerup", up);
379
+ }
380
+
381
+ return (
382
+ <div onPointerDown={onPointerDown} title="Drag to resize" style={{ width: 6, flex: "0 0 6px", cursor: "col-resize", background: "#241e2c", position: "relative", zIndex: 5 }}>
383
+ <div style={{ position: "absolute", left: "50%", top: "50%", transform: "translate(-50%,-50%)", width: 3, height: 44, borderRadius: 3, background: "#4a4058" }} />
384
+ </div>
385
+ );
386
+ }
387
+
388
+ function now(): string {
389
+ return new Date().toTimeString().slice(0, 5);
390
+ }