Mirae Kang commited on
Commit
f0b240d
·
1 Parent(s): a938849

feat: update UI using VITE+React without streamlit, #22

Browse files
frontend/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules
2
+ dist
3
+ *.local
frontend/index.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>youtube_hate_detector</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ <script type="module" src="/src/main.tsx"></script>
17
+ </body>
18
+ </html>
frontend/logs/pipeline_20260523.log ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 2026-05-23 22:06:09 | INFO | src.api.main | Starting youtube_hate_detector API — model: LR + TF-IDF (local)
2
+ 2026-05-23 22:06:11 | INFO | src.api.main | Model warm-up complete
3
+ 2026-05-23 22:09:18 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
4
+ 2026-05-23 22:09:56 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
5
+ 2026-05-23 22:10:21 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
6
+ 2026-05-23 23:08:59 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
7
+ 2026-05-23 23:10:24 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
8
+ 2026-05-23 23:10:26 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
9
+ 2026-05-23 23:10:26 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
10
+ 2026-05-23 23:10:27 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
11
+ 2026-05-23 23:10:28 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
12
+ 2026-05-23 23:10:28 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
13
+ 2026-05-23 23:10:58 | INFO | src.api.main | API shutdown
14
+ 2026-05-23 23:12:49 | INFO | src.api.main | Starting youtube_hate_detector API — model: LR + TF-IDF (local)
15
+ 2026-05-23 23:12:51 | INFO | src.api.main | Model warm-up complete
16
+ 2026-05-23 23:12:52 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
17
+ 2026-05-23 23:12:57 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
18
+ 2026-05-23 23:13:00 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
19
+ 2026-05-23 23:13:01 | WARNING | src.api.youtube | YOUTUBE_API_KEY not set — using demo comments
20
+ 2026-05-23 23:27:19 | INFO | src.api.main | API shutdown
frontend/logs/pipeline_20260524.log ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ 2026-05-24 15:06:47 | INFO | src.api.main | Starting youtube_hate_detector API — model: LR + TF-IDF (local)
2
+ 2026-05-24 15:06:54 | INFO | src.api.main | Model warm-up complete
3
+ 2026-05-24 15:07:02 | INFO | src.api.main | API shutdown
frontend/package-lock.json ADDED
@@ -0,0 +1,2302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "youtube_hate_detector-ui",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "youtube_hate_detector-ui",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0",
13
+ "react-router-dom": "^7.1.0",
14
+ "recharts": "^2.15.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/react": "^19.0.0",
18
+ "@types/react-dom": "^19.0.0",
19
+ "@vitejs/plugin-react": "^4.3.4",
20
+ "typescript": "~5.7.2",
21
+ "vite": "^6.0.0"
22
+ }
23
+ },
24
+ "node_modules/@babel/code-frame": {
25
+ "version": "7.29.0",
26
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
27
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
28
+ "dev": true,
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@babel/helper-validator-identifier": "^7.28.5",
32
+ "js-tokens": "^4.0.0",
33
+ "picocolors": "^1.1.1"
34
+ },
35
+ "engines": {
36
+ "node": ">=6.9.0"
37
+ }
38
+ },
39
+ "node_modules/@babel/compat-data": {
40
+ "version": "7.29.3",
41
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
42
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
43
+ "dev": true,
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": ">=6.9.0"
47
+ }
48
+ },
49
+ "node_modules/@babel/core": {
50
+ "version": "7.29.0",
51
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
52
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
53
+ "dev": true,
54
+ "license": "MIT",
55
+ "dependencies": {
56
+ "@babel/code-frame": "^7.29.0",
57
+ "@babel/generator": "^7.29.0",
58
+ "@babel/helper-compilation-targets": "^7.28.6",
59
+ "@babel/helper-module-transforms": "^7.28.6",
60
+ "@babel/helpers": "^7.28.6",
61
+ "@babel/parser": "^7.29.0",
62
+ "@babel/template": "^7.28.6",
63
+ "@babel/traverse": "^7.29.0",
64
+ "@babel/types": "^7.29.0",
65
+ "@jridgewell/remapping": "^2.3.5",
66
+ "convert-source-map": "^2.0.0",
67
+ "debug": "^4.1.0",
68
+ "gensync": "^1.0.0-beta.2",
69
+ "json5": "^2.2.3",
70
+ "semver": "^6.3.1"
71
+ },
72
+ "engines": {
73
+ "node": ">=6.9.0"
74
+ },
75
+ "funding": {
76
+ "type": "opencollective",
77
+ "url": "https://opencollective.com/babel"
78
+ }
79
+ },
80
+ "node_modules/@babel/generator": {
81
+ "version": "7.29.1",
82
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
83
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
84
+ "dev": true,
85
+ "license": "MIT",
86
+ "dependencies": {
87
+ "@babel/parser": "^7.29.0",
88
+ "@babel/types": "^7.29.0",
89
+ "@jridgewell/gen-mapping": "^0.3.12",
90
+ "@jridgewell/trace-mapping": "^0.3.28",
91
+ "jsesc": "^3.0.2"
92
+ },
93
+ "engines": {
94
+ "node": ">=6.9.0"
95
+ }
96
+ },
97
+ "node_modules/@babel/helper-compilation-targets": {
98
+ "version": "7.28.6",
99
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
100
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
101
+ "dev": true,
102
+ "license": "MIT",
103
+ "dependencies": {
104
+ "@babel/compat-data": "^7.28.6",
105
+ "@babel/helper-validator-option": "^7.27.1",
106
+ "browserslist": "^4.24.0",
107
+ "lru-cache": "^5.1.1",
108
+ "semver": "^6.3.1"
109
+ },
110
+ "engines": {
111
+ "node": ">=6.9.0"
112
+ }
113
+ },
114
+ "node_modules/@babel/helper-globals": {
115
+ "version": "7.28.0",
116
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
117
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
118
+ "dev": true,
119
+ "license": "MIT",
120
+ "engines": {
121
+ "node": ">=6.9.0"
122
+ }
123
+ },
124
+ "node_modules/@babel/helper-module-imports": {
125
+ "version": "7.28.6",
126
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
127
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
128
+ "dev": true,
129
+ "license": "MIT",
130
+ "dependencies": {
131
+ "@babel/traverse": "^7.28.6",
132
+ "@babel/types": "^7.28.6"
133
+ },
134
+ "engines": {
135
+ "node": ">=6.9.0"
136
+ }
137
+ },
138
+ "node_modules/@babel/helper-module-transforms": {
139
+ "version": "7.28.6",
140
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
141
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
142
+ "dev": true,
143
+ "license": "MIT",
144
+ "dependencies": {
145
+ "@babel/helper-module-imports": "^7.28.6",
146
+ "@babel/helper-validator-identifier": "^7.28.5",
147
+ "@babel/traverse": "^7.28.6"
148
+ },
149
+ "engines": {
150
+ "node": ">=6.9.0"
151
+ },
152
+ "peerDependencies": {
153
+ "@babel/core": "^7.0.0"
154
+ }
155
+ },
156
+ "node_modules/@babel/helper-plugin-utils": {
157
+ "version": "7.28.6",
158
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
159
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
160
+ "dev": true,
161
+ "license": "MIT",
162
+ "engines": {
163
+ "node": ">=6.9.0"
164
+ }
165
+ },
166
+ "node_modules/@babel/helper-string-parser": {
167
+ "version": "7.27.1",
168
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
169
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
170
+ "dev": true,
171
+ "license": "MIT",
172
+ "engines": {
173
+ "node": ">=6.9.0"
174
+ }
175
+ },
176
+ "node_modules/@babel/helper-validator-identifier": {
177
+ "version": "7.28.5",
178
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
179
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
180
+ "dev": true,
181
+ "license": "MIT",
182
+ "engines": {
183
+ "node": ">=6.9.0"
184
+ }
185
+ },
186
+ "node_modules/@babel/helper-validator-option": {
187
+ "version": "7.27.1",
188
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
189
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
190
+ "dev": true,
191
+ "license": "MIT",
192
+ "engines": {
193
+ "node": ">=6.9.0"
194
+ }
195
+ },
196
+ "node_modules/@babel/helpers": {
197
+ "version": "7.29.2",
198
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
199
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
200
+ "dev": true,
201
+ "license": "MIT",
202
+ "dependencies": {
203
+ "@babel/template": "^7.28.6",
204
+ "@babel/types": "^7.29.0"
205
+ },
206
+ "engines": {
207
+ "node": ">=6.9.0"
208
+ }
209
+ },
210
+ "node_modules/@babel/parser": {
211
+ "version": "7.29.3",
212
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
213
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
214
+ "dev": true,
215
+ "license": "MIT",
216
+ "dependencies": {
217
+ "@babel/types": "^7.29.0"
218
+ },
219
+ "bin": {
220
+ "parser": "bin/babel-parser.js"
221
+ },
222
+ "engines": {
223
+ "node": ">=6.0.0"
224
+ }
225
+ },
226
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
227
+ "version": "7.27.1",
228
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
229
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
230
+ "dev": true,
231
+ "license": "MIT",
232
+ "dependencies": {
233
+ "@babel/helper-plugin-utils": "^7.27.1"
234
+ },
235
+ "engines": {
236
+ "node": ">=6.9.0"
237
+ },
238
+ "peerDependencies": {
239
+ "@babel/core": "^7.0.0-0"
240
+ }
241
+ },
242
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
243
+ "version": "7.27.1",
244
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
245
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
246
+ "dev": true,
247
+ "license": "MIT",
248
+ "dependencies": {
249
+ "@babel/helper-plugin-utils": "^7.27.1"
250
+ },
251
+ "engines": {
252
+ "node": ">=6.9.0"
253
+ },
254
+ "peerDependencies": {
255
+ "@babel/core": "^7.0.0-0"
256
+ }
257
+ },
258
+ "node_modules/@babel/runtime": {
259
+ "version": "7.29.2",
260
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
261
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
262
+ "license": "MIT",
263
+ "engines": {
264
+ "node": ">=6.9.0"
265
+ }
266
+ },
267
+ "node_modules/@babel/template": {
268
+ "version": "7.28.6",
269
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
270
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
271
+ "dev": true,
272
+ "license": "MIT",
273
+ "dependencies": {
274
+ "@babel/code-frame": "^7.28.6",
275
+ "@babel/parser": "^7.28.6",
276
+ "@babel/types": "^7.28.6"
277
+ },
278
+ "engines": {
279
+ "node": ">=6.9.0"
280
+ }
281
+ },
282
+ "node_modules/@babel/traverse": {
283
+ "version": "7.29.0",
284
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
285
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
286
+ "dev": true,
287
+ "license": "MIT",
288
+ "dependencies": {
289
+ "@babel/code-frame": "^7.29.0",
290
+ "@babel/generator": "^7.29.0",
291
+ "@babel/helper-globals": "^7.28.0",
292
+ "@babel/parser": "^7.29.0",
293
+ "@babel/template": "^7.28.6",
294
+ "@babel/types": "^7.29.0",
295
+ "debug": "^4.3.1"
296
+ },
297
+ "engines": {
298
+ "node": ">=6.9.0"
299
+ }
300
+ },
301
+ "node_modules/@babel/types": {
302
+ "version": "7.29.0",
303
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
304
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
305
+ "dev": true,
306
+ "license": "MIT",
307
+ "dependencies": {
308
+ "@babel/helper-string-parser": "^7.27.1",
309
+ "@babel/helper-validator-identifier": "^7.28.5"
310
+ },
311
+ "engines": {
312
+ "node": ">=6.9.0"
313
+ }
314
+ },
315
+ "node_modules/@esbuild/aix-ppc64": {
316
+ "version": "0.25.12",
317
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
318
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
319
+ "cpu": [
320
+ "ppc64"
321
+ ],
322
+ "dev": true,
323
+ "license": "MIT",
324
+ "optional": true,
325
+ "os": [
326
+ "aix"
327
+ ],
328
+ "engines": {
329
+ "node": ">=18"
330
+ }
331
+ },
332
+ "node_modules/@esbuild/android-arm": {
333
+ "version": "0.25.12",
334
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
335
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
336
+ "cpu": [
337
+ "arm"
338
+ ],
339
+ "dev": true,
340
+ "license": "MIT",
341
+ "optional": true,
342
+ "os": [
343
+ "android"
344
+ ],
345
+ "engines": {
346
+ "node": ">=18"
347
+ }
348
+ },
349
+ "node_modules/@esbuild/android-arm64": {
350
+ "version": "0.25.12",
351
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
352
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
353
+ "cpu": [
354
+ "arm64"
355
+ ],
356
+ "dev": true,
357
+ "license": "MIT",
358
+ "optional": true,
359
+ "os": [
360
+ "android"
361
+ ],
362
+ "engines": {
363
+ "node": ">=18"
364
+ }
365
+ },
366
+ "node_modules/@esbuild/android-x64": {
367
+ "version": "0.25.12",
368
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
369
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
370
+ "cpu": [
371
+ "x64"
372
+ ],
373
+ "dev": true,
374
+ "license": "MIT",
375
+ "optional": true,
376
+ "os": [
377
+ "android"
378
+ ],
379
+ "engines": {
380
+ "node": ">=18"
381
+ }
382
+ },
383
+ "node_modules/@esbuild/darwin-arm64": {
384
+ "version": "0.25.12",
385
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
386
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
387
+ "cpu": [
388
+ "arm64"
389
+ ],
390
+ "dev": true,
391
+ "license": "MIT",
392
+ "optional": true,
393
+ "os": [
394
+ "darwin"
395
+ ],
396
+ "engines": {
397
+ "node": ">=18"
398
+ }
399
+ },
400
+ "node_modules/@esbuild/darwin-x64": {
401
+ "version": "0.25.12",
402
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
403
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
404
+ "cpu": [
405
+ "x64"
406
+ ],
407
+ "dev": true,
408
+ "license": "MIT",
409
+ "optional": true,
410
+ "os": [
411
+ "darwin"
412
+ ],
413
+ "engines": {
414
+ "node": ">=18"
415
+ }
416
+ },
417
+ "node_modules/@esbuild/freebsd-arm64": {
418
+ "version": "0.25.12",
419
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
420
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
421
+ "cpu": [
422
+ "arm64"
423
+ ],
424
+ "dev": true,
425
+ "license": "MIT",
426
+ "optional": true,
427
+ "os": [
428
+ "freebsd"
429
+ ],
430
+ "engines": {
431
+ "node": ">=18"
432
+ }
433
+ },
434
+ "node_modules/@esbuild/freebsd-x64": {
435
+ "version": "0.25.12",
436
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
437
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
438
+ "cpu": [
439
+ "x64"
440
+ ],
441
+ "dev": true,
442
+ "license": "MIT",
443
+ "optional": true,
444
+ "os": [
445
+ "freebsd"
446
+ ],
447
+ "engines": {
448
+ "node": ">=18"
449
+ }
450
+ },
451
+ "node_modules/@esbuild/linux-arm": {
452
+ "version": "0.25.12",
453
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
454
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
455
+ "cpu": [
456
+ "arm"
457
+ ],
458
+ "dev": true,
459
+ "license": "MIT",
460
+ "optional": true,
461
+ "os": [
462
+ "linux"
463
+ ],
464
+ "engines": {
465
+ "node": ">=18"
466
+ }
467
+ },
468
+ "node_modules/@esbuild/linux-arm64": {
469
+ "version": "0.25.12",
470
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
471
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
472
+ "cpu": [
473
+ "arm64"
474
+ ],
475
+ "dev": true,
476
+ "license": "MIT",
477
+ "optional": true,
478
+ "os": [
479
+ "linux"
480
+ ],
481
+ "engines": {
482
+ "node": ">=18"
483
+ }
484
+ },
485
+ "node_modules/@esbuild/linux-ia32": {
486
+ "version": "0.25.12",
487
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
488
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
489
+ "cpu": [
490
+ "ia32"
491
+ ],
492
+ "dev": true,
493
+ "license": "MIT",
494
+ "optional": true,
495
+ "os": [
496
+ "linux"
497
+ ],
498
+ "engines": {
499
+ "node": ">=18"
500
+ }
501
+ },
502
+ "node_modules/@esbuild/linux-loong64": {
503
+ "version": "0.25.12",
504
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
505
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
506
+ "cpu": [
507
+ "loong64"
508
+ ],
509
+ "dev": true,
510
+ "license": "MIT",
511
+ "optional": true,
512
+ "os": [
513
+ "linux"
514
+ ],
515
+ "engines": {
516
+ "node": ">=18"
517
+ }
518
+ },
519
+ "node_modules/@esbuild/linux-mips64el": {
520
+ "version": "0.25.12",
521
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
522
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
523
+ "cpu": [
524
+ "mips64el"
525
+ ],
526
+ "dev": true,
527
+ "license": "MIT",
528
+ "optional": true,
529
+ "os": [
530
+ "linux"
531
+ ],
532
+ "engines": {
533
+ "node": ">=18"
534
+ }
535
+ },
536
+ "node_modules/@esbuild/linux-ppc64": {
537
+ "version": "0.25.12",
538
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
539
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
540
+ "cpu": [
541
+ "ppc64"
542
+ ],
543
+ "dev": true,
544
+ "license": "MIT",
545
+ "optional": true,
546
+ "os": [
547
+ "linux"
548
+ ],
549
+ "engines": {
550
+ "node": ">=18"
551
+ }
552
+ },
553
+ "node_modules/@esbuild/linux-riscv64": {
554
+ "version": "0.25.12",
555
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
556
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
557
+ "cpu": [
558
+ "riscv64"
559
+ ],
560
+ "dev": true,
561
+ "license": "MIT",
562
+ "optional": true,
563
+ "os": [
564
+ "linux"
565
+ ],
566
+ "engines": {
567
+ "node": ">=18"
568
+ }
569
+ },
570
+ "node_modules/@esbuild/linux-s390x": {
571
+ "version": "0.25.12",
572
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
573
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
574
+ "cpu": [
575
+ "s390x"
576
+ ],
577
+ "dev": true,
578
+ "license": "MIT",
579
+ "optional": true,
580
+ "os": [
581
+ "linux"
582
+ ],
583
+ "engines": {
584
+ "node": ">=18"
585
+ }
586
+ },
587
+ "node_modules/@esbuild/linux-x64": {
588
+ "version": "0.25.12",
589
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
590
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
591
+ "cpu": [
592
+ "x64"
593
+ ],
594
+ "dev": true,
595
+ "license": "MIT",
596
+ "optional": true,
597
+ "os": [
598
+ "linux"
599
+ ],
600
+ "engines": {
601
+ "node": ">=18"
602
+ }
603
+ },
604
+ "node_modules/@esbuild/netbsd-arm64": {
605
+ "version": "0.25.12",
606
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
607
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
608
+ "cpu": [
609
+ "arm64"
610
+ ],
611
+ "dev": true,
612
+ "license": "MIT",
613
+ "optional": true,
614
+ "os": [
615
+ "netbsd"
616
+ ],
617
+ "engines": {
618
+ "node": ">=18"
619
+ }
620
+ },
621
+ "node_modules/@esbuild/netbsd-x64": {
622
+ "version": "0.25.12",
623
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
624
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
625
+ "cpu": [
626
+ "x64"
627
+ ],
628
+ "dev": true,
629
+ "license": "MIT",
630
+ "optional": true,
631
+ "os": [
632
+ "netbsd"
633
+ ],
634
+ "engines": {
635
+ "node": ">=18"
636
+ }
637
+ },
638
+ "node_modules/@esbuild/openbsd-arm64": {
639
+ "version": "0.25.12",
640
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
641
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
642
+ "cpu": [
643
+ "arm64"
644
+ ],
645
+ "dev": true,
646
+ "license": "MIT",
647
+ "optional": true,
648
+ "os": [
649
+ "openbsd"
650
+ ],
651
+ "engines": {
652
+ "node": ">=18"
653
+ }
654
+ },
655
+ "node_modules/@esbuild/openbsd-x64": {
656
+ "version": "0.25.12",
657
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
658
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
659
+ "cpu": [
660
+ "x64"
661
+ ],
662
+ "dev": true,
663
+ "license": "MIT",
664
+ "optional": true,
665
+ "os": [
666
+ "openbsd"
667
+ ],
668
+ "engines": {
669
+ "node": ">=18"
670
+ }
671
+ },
672
+ "node_modules/@esbuild/openharmony-arm64": {
673
+ "version": "0.25.12",
674
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
675
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
676
+ "cpu": [
677
+ "arm64"
678
+ ],
679
+ "dev": true,
680
+ "license": "MIT",
681
+ "optional": true,
682
+ "os": [
683
+ "openharmony"
684
+ ],
685
+ "engines": {
686
+ "node": ">=18"
687
+ }
688
+ },
689
+ "node_modules/@esbuild/sunos-x64": {
690
+ "version": "0.25.12",
691
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
692
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
693
+ "cpu": [
694
+ "x64"
695
+ ],
696
+ "dev": true,
697
+ "license": "MIT",
698
+ "optional": true,
699
+ "os": [
700
+ "sunos"
701
+ ],
702
+ "engines": {
703
+ "node": ">=18"
704
+ }
705
+ },
706
+ "node_modules/@esbuild/win32-arm64": {
707
+ "version": "0.25.12",
708
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
709
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
710
+ "cpu": [
711
+ "arm64"
712
+ ],
713
+ "dev": true,
714
+ "license": "MIT",
715
+ "optional": true,
716
+ "os": [
717
+ "win32"
718
+ ],
719
+ "engines": {
720
+ "node": ">=18"
721
+ }
722
+ },
723
+ "node_modules/@esbuild/win32-ia32": {
724
+ "version": "0.25.12",
725
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
726
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
727
+ "cpu": [
728
+ "ia32"
729
+ ],
730
+ "dev": true,
731
+ "license": "MIT",
732
+ "optional": true,
733
+ "os": [
734
+ "win32"
735
+ ],
736
+ "engines": {
737
+ "node": ">=18"
738
+ }
739
+ },
740
+ "node_modules/@esbuild/win32-x64": {
741
+ "version": "0.25.12",
742
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
743
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
744
+ "cpu": [
745
+ "x64"
746
+ ],
747
+ "dev": true,
748
+ "license": "MIT",
749
+ "optional": true,
750
+ "os": [
751
+ "win32"
752
+ ],
753
+ "engines": {
754
+ "node": ">=18"
755
+ }
756
+ },
757
+ "node_modules/@jridgewell/gen-mapping": {
758
+ "version": "0.3.13",
759
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
760
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
761
+ "dev": true,
762
+ "license": "MIT",
763
+ "dependencies": {
764
+ "@jridgewell/sourcemap-codec": "^1.5.0",
765
+ "@jridgewell/trace-mapping": "^0.3.24"
766
+ }
767
+ },
768
+ "node_modules/@jridgewell/remapping": {
769
+ "version": "2.3.5",
770
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
771
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
772
+ "dev": true,
773
+ "license": "MIT",
774
+ "dependencies": {
775
+ "@jridgewell/gen-mapping": "^0.3.5",
776
+ "@jridgewell/trace-mapping": "^0.3.24"
777
+ }
778
+ },
779
+ "node_modules/@jridgewell/resolve-uri": {
780
+ "version": "3.1.2",
781
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
782
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
783
+ "dev": true,
784
+ "license": "MIT",
785
+ "engines": {
786
+ "node": ">=6.0.0"
787
+ }
788
+ },
789
+ "node_modules/@jridgewell/sourcemap-codec": {
790
+ "version": "1.5.5",
791
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
792
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
793
+ "dev": true,
794
+ "license": "MIT"
795
+ },
796
+ "node_modules/@jridgewell/trace-mapping": {
797
+ "version": "0.3.31",
798
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
799
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
800
+ "dev": true,
801
+ "license": "MIT",
802
+ "dependencies": {
803
+ "@jridgewell/resolve-uri": "^3.1.0",
804
+ "@jridgewell/sourcemap-codec": "^1.4.14"
805
+ }
806
+ },
807
+ "node_modules/@rolldown/pluginutils": {
808
+ "version": "1.0.0-beta.27",
809
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
810
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
811
+ "dev": true,
812
+ "license": "MIT"
813
+ },
814
+ "node_modules/@rollup/rollup-android-arm-eabi": {
815
+ "version": "4.60.4",
816
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
817
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
818
+ "cpu": [
819
+ "arm"
820
+ ],
821
+ "dev": true,
822
+ "license": "MIT",
823
+ "optional": true,
824
+ "os": [
825
+ "android"
826
+ ]
827
+ },
828
+ "node_modules/@rollup/rollup-android-arm64": {
829
+ "version": "4.60.4",
830
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
831
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
832
+ "cpu": [
833
+ "arm64"
834
+ ],
835
+ "dev": true,
836
+ "license": "MIT",
837
+ "optional": true,
838
+ "os": [
839
+ "android"
840
+ ]
841
+ },
842
+ "node_modules/@rollup/rollup-darwin-arm64": {
843
+ "version": "4.60.4",
844
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
845
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
846
+ "cpu": [
847
+ "arm64"
848
+ ],
849
+ "dev": true,
850
+ "license": "MIT",
851
+ "optional": true,
852
+ "os": [
853
+ "darwin"
854
+ ]
855
+ },
856
+ "node_modules/@rollup/rollup-darwin-x64": {
857
+ "version": "4.60.4",
858
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
859
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
860
+ "cpu": [
861
+ "x64"
862
+ ],
863
+ "dev": true,
864
+ "license": "MIT",
865
+ "optional": true,
866
+ "os": [
867
+ "darwin"
868
+ ]
869
+ },
870
+ "node_modules/@rollup/rollup-freebsd-arm64": {
871
+ "version": "4.60.4",
872
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
873
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
874
+ "cpu": [
875
+ "arm64"
876
+ ],
877
+ "dev": true,
878
+ "license": "MIT",
879
+ "optional": true,
880
+ "os": [
881
+ "freebsd"
882
+ ]
883
+ },
884
+ "node_modules/@rollup/rollup-freebsd-x64": {
885
+ "version": "4.60.4",
886
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
887
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
888
+ "cpu": [
889
+ "x64"
890
+ ],
891
+ "dev": true,
892
+ "license": "MIT",
893
+ "optional": true,
894
+ "os": [
895
+ "freebsd"
896
+ ]
897
+ },
898
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
899
+ "version": "4.60.4",
900
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
901
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
902
+ "cpu": [
903
+ "arm"
904
+ ],
905
+ "dev": true,
906
+ "libc": [
907
+ "glibc"
908
+ ],
909
+ "license": "MIT",
910
+ "optional": true,
911
+ "os": [
912
+ "linux"
913
+ ]
914
+ },
915
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
916
+ "version": "4.60.4",
917
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
918
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
919
+ "cpu": [
920
+ "arm"
921
+ ],
922
+ "dev": true,
923
+ "libc": [
924
+ "musl"
925
+ ],
926
+ "license": "MIT",
927
+ "optional": true,
928
+ "os": [
929
+ "linux"
930
+ ]
931
+ },
932
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
933
+ "version": "4.60.4",
934
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
935
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
936
+ "cpu": [
937
+ "arm64"
938
+ ],
939
+ "dev": true,
940
+ "libc": [
941
+ "glibc"
942
+ ],
943
+ "license": "MIT",
944
+ "optional": true,
945
+ "os": [
946
+ "linux"
947
+ ]
948
+ },
949
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
950
+ "version": "4.60.4",
951
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
952
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
953
+ "cpu": [
954
+ "arm64"
955
+ ],
956
+ "dev": true,
957
+ "libc": [
958
+ "musl"
959
+ ],
960
+ "license": "MIT",
961
+ "optional": true,
962
+ "os": [
963
+ "linux"
964
+ ]
965
+ },
966
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
967
+ "version": "4.60.4",
968
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
969
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
970
+ "cpu": [
971
+ "loong64"
972
+ ],
973
+ "dev": true,
974
+ "libc": [
975
+ "glibc"
976
+ ],
977
+ "license": "MIT",
978
+ "optional": true,
979
+ "os": [
980
+ "linux"
981
+ ]
982
+ },
983
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
984
+ "version": "4.60.4",
985
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
986
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
987
+ "cpu": [
988
+ "loong64"
989
+ ],
990
+ "dev": true,
991
+ "libc": [
992
+ "musl"
993
+ ],
994
+ "license": "MIT",
995
+ "optional": true,
996
+ "os": [
997
+ "linux"
998
+ ]
999
+ },
1000
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1001
+ "version": "4.60.4",
1002
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
1003
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
1004
+ "cpu": [
1005
+ "ppc64"
1006
+ ],
1007
+ "dev": true,
1008
+ "libc": [
1009
+ "glibc"
1010
+ ],
1011
+ "license": "MIT",
1012
+ "optional": true,
1013
+ "os": [
1014
+ "linux"
1015
+ ]
1016
+ },
1017
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
1018
+ "version": "4.60.4",
1019
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
1020
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
1021
+ "cpu": [
1022
+ "ppc64"
1023
+ ],
1024
+ "dev": true,
1025
+ "libc": [
1026
+ "musl"
1027
+ ],
1028
+ "license": "MIT",
1029
+ "optional": true,
1030
+ "os": [
1031
+ "linux"
1032
+ ]
1033
+ },
1034
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1035
+ "version": "4.60.4",
1036
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
1037
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
1038
+ "cpu": [
1039
+ "riscv64"
1040
+ ],
1041
+ "dev": true,
1042
+ "libc": [
1043
+ "glibc"
1044
+ ],
1045
+ "license": "MIT",
1046
+ "optional": true,
1047
+ "os": [
1048
+ "linux"
1049
+ ]
1050
+ },
1051
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
1052
+ "version": "4.60.4",
1053
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
1054
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
1055
+ "cpu": [
1056
+ "riscv64"
1057
+ ],
1058
+ "dev": true,
1059
+ "libc": [
1060
+ "musl"
1061
+ ],
1062
+ "license": "MIT",
1063
+ "optional": true,
1064
+ "os": [
1065
+ "linux"
1066
+ ]
1067
+ },
1068
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
1069
+ "version": "4.60.4",
1070
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
1071
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
1072
+ "cpu": [
1073
+ "s390x"
1074
+ ],
1075
+ "dev": true,
1076
+ "libc": [
1077
+ "glibc"
1078
+ ],
1079
+ "license": "MIT",
1080
+ "optional": true,
1081
+ "os": [
1082
+ "linux"
1083
+ ]
1084
+ },
1085
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
1086
+ "version": "4.60.4",
1087
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
1088
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
1089
+ "cpu": [
1090
+ "x64"
1091
+ ],
1092
+ "dev": true,
1093
+ "libc": [
1094
+ "glibc"
1095
+ ],
1096
+ "license": "MIT",
1097
+ "optional": true,
1098
+ "os": [
1099
+ "linux"
1100
+ ]
1101
+ },
1102
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1103
+ "version": "4.60.4",
1104
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
1105
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
1106
+ "cpu": [
1107
+ "x64"
1108
+ ],
1109
+ "dev": true,
1110
+ "libc": [
1111
+ "musl"
1112
+ ],
1113
+ "license": "MIT",
1114
+ "optional": true,
1115
+ "os": [
1116
+ "linux"
1117
+ ]
1118
+ },
1119
+ "node_modules/@rollup/rollup-openbsd-x64": {
1120
+ "version": "4.60.4",
1121
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
1122
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
1123
+ "cpu": [
1124
+ "x64"
1125
+ ],
1126
+ "dev": true,
1127
+ "license": "MIT",
1128
+ "optional": true,
1129
+ "os": [
1130
+ "openbsd"
1131
+ ]
1132
+ },
1133
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1134
+ "version": "4.60.4",
1135
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
1136
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
1137
+ "cpu": [
1138
+ "arm64"
1139
+ ],
1140
+ "dev": true,
1141
+ "license": "MIT",
1142
+ "optional": true,
1143
+ "os": [
1144
+ "openharmony"
1145
+ ]
1146
+ },
1147
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1148
+ "version": "4.60.4",
1149
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
1150
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
1151
+ "cpu": [
1152
+ "arm64"
1153
+ ],
1154
+ "dev": true,
1155
+ "license": "MIT",
1156
+ "optional": true,
1157
+ "os": [
1158
+ "win32"
1159
+ ]
1160
+ },
1161
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1162
+ "version": "4.60.4",
1163
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
1164
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
1165
+ "cpu": [
1166
+ "ia32"
1167
+ ],
1168
+ "dev": true,
1169
+ "license": "MIT",
1170
+ "optional": true,
1171
+ "os": [
1172
+ "win32"
1173
+ ]
1174
+ },
1175
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1176
+ "version": "4.60.4",
1177
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
1178
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
1179
+ "cpu": [
1180
+ "x64"
1181
+ ],
1182
+ "dev": true,
1183
+ "license": "MIT",
1184
+ "optional": true,
1185
+ "os": [
1186
+ "win32"
1187
+ ]
1188
+ },
1189
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1190
+ "version": "4.60.4",
1191
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
1192
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
1193
+ "cpu": [
1194
+ "x64"
1195
+ ],
1196
+ "dev": true,
1197
+ "license": "MIT",
1198
+ "optional": true,
1199
+ "os": [
1200
+ "win32"
1201
+ ]
1202
+ },
1203
+ "node_modules/@types/babel__core": {
1204
+ "version": "7.20.5",
1205
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1206
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1207
+ "dev": true,
1208
+ "license": "MIT",
1209
+ "dependencies": {
1210
+ "@babel/parser": "^7.20.7",
1211
+ "@babel/types": "^7.20.7",
1212
+ "@types/babel__generator": "*",
1213
+ "@types/babel__template": "*",
1214
+ "@types/babel__traverse": "*"
1215
+ }
1216
+ },
1217
+ "node_modules/@types/babel__generator": {
1218
+ "version": "7.27.0",
1219
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1220
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1221
+ "dev": true,
1222
+ "license": "MIT",
1223
+ "dependencies": {
1224
+ "@babel/types": "^7.0.0"
1225
+ }
1226
+ },
1227
+ "node_modules/@types/babel__template": {
1228
+ "version": "7.4.4",
1229
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1230
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1231
+ "dev": true,
1232
+ "license": "MIT",
1233
+ "dependencies": {
1234
+ "@babel/parser": "^7.1.0",
1235
+ "@babel/types": "^7.0.0"
1236
+ }
1237
+ },
1238
+ "node_modules/@types/babel__traverse": {
1239
+ "version": "7.28.0",
1240
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1241
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1242
+ "dev": true,
1243
+ "license": "MIT",
1244
+ "dependencies": {
1245
+ "@babel/types": "^7.28.2"
1246
+ }
1247
+ },
1248
+ "node_modules/@types/d3-array": {
1249
+ "version": "3.2.2",
1250
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
1251
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
1252
+ "license": "MIT"
1253
+ },
1254
+ "node_modules/@types/d3-color": {
1255
+ "version": "3.1.3",
1256
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
1257
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
1258
+ "license": "MIT"
1259
+ },
1260
+ "node_modules/@types/d3-ease": {
1261
+ "version": "3.0.2",
1262
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
1263
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
1264
+ "license": "MIT"
1265
+ },
1266
+ "node_modules/@types/d3-interpolate": {
1267
+ "version": "3.0.4",
1268
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
1269
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
1270
+ "license": "MIT",
1271
+ "dependencies": {
1272
+ "@types/d3-color": "*"
1273
+ }
1274
+ },
1275
+ "node_modules/@types/d3-path": {
1276
+ "version": "3.1.1",
1277
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
1278
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
1279
+ "license": "MIT"
1280
+ },
1281
+ "node_modules/@types/d3-scale": {
1282
+ "version": "4.0.9",
1283
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
1284
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
1285
+ "license": "MIT",
1286
+ "dependencies": {
1287
+ "@types/d3-time": "*"
1288
+ }
1289
+ },
1290
+ "node_modules/@types/d3-shape": {
1291
+ "version": "3.1.8",
1292
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
1293
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
1294
+ "license": "MIT",
1295
+ "dependencies": {
1296
+ "@types/d3-path": "*"
1297
+ }
1298
+ },
1299
+ "node_modules/@types/d3-time": {
1300
+ "version": "3.0.4",
1301
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
1302
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
1303
+ "license": "MIT"
1304
+ },
1305
+ "node_modules/@types/d3-timer": {
1306
+ "version": "3.0.2",
1307
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
1308
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
1309
+ "license": "MIT"
1310
+ },
1311
+ "node_modules/@types/estree": {
1312
+ "version": "1.0.8",
1313
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1314
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1315
+ "dev": true,
1316
+ "license": "MIT"
1317
+ },
1318
+ "node_modules/@types/react": {
1319
+ "version": "19.2.15",
1320
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
1321
+ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
1322
+ "dev": true,
1323
+ "license": "MIT",
1324
+ "dependencies": {
1325
+ "csstype": "^3.2.2"
1326
+ }
1327
+ },
1328
+ "node_modules/@types/react-dom": {
1329
+ "version": "19.2.3",
1330
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
1331
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
1332
+ "dev": true,
1333
+ "license": "MIT",
1334
+ "peerDependencies": {
1335
+ "@types/react": "^19.2.0"
1336
+ }
1337
+ },
1338
+ "node_modules/@vitejs/plugin-react": {
1339
+ "version": "4.7.0",
1340
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1341
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1342
+ "dev": true,
1343
+ "license": "MIT",
1344
+ "dependencies": {
1345
+ "@babel/core": "^7.28.0",
1346
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1347
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1348
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1349
+ "@types/babel__core": "^7.20.5",
1350
+ "react-refresh": "^0.17.0"
1351
+ },
1352
+ "engines": {
1353
+ "node": "^14.18.0 || >=16.0.0"
1354
+ },
1355
+ "peerDependencies": {
1356
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1357
+ }
1358
+ },
1359
+ "node_modules/baseline-browser-mapping": {
1360
+ "version": "2.10.32",
1361
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
1362
+ "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==",
1363
+ "dev": true,
1364
+ "license": "Apache-2.0",
1365
+ "bin": {
1366
+ "baseline-browser-mapping": "dist/cli.cjs"
1367
+ },
1368
+ "engines": {
1369
+ "node": ">=6.0.0"
1370
+ }
1371
+ },
1372
+ "node_modules/browserslist": {
1373
+ "version": "4.28.2",
1374
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1375
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1376
+ "dev": true,
1377
+ "funding": [
1378
+ {
1379
+ "type": "opencollective",
1380
+ "url": "https://opencollective.com/browserslist"
1381
+ },
1382
+ {
1383
+ "type": "tidelift",
1384
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1385
+ },
1386
+ {
1387
+ "type": "github",
1388
+ "url": "https://github.com/sponsors/ai"
1389
+ }
1390
+ ],
1391
+ "license": "MIT",
1392
+ "dependencies": {
1393
+ "baseline-browser-mapping": "^2.10.12",
1394
+ "caniuse-lite": "^1.0.30001782",
1395
+ "electron-to-chromium": "^1.5.328",
1396
+ "node-releases": "^2.0.36",
1397
+ "update-browserslist-db": "^1.2.3"
1398
+ },
1399
+ "bin": {
1400
+ "browserslist": "cli.js"
1401
+ },
1402
+ "engines": {
1403
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1404
+ }
1405
+ },
1406
+ "node_modules/caniuse-lite": {
1407
+ "version": "1.0.30001793",
1408
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
1409
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
1410
+ "dev": true,
1411
+ "funding": [
1412
+ {
1413
+ "type": "opencollective",
1414
+ "url": "https://opencollective.com/browserslist"
1415
+ },
1416
+ {
1417
+ "type": "tidelift",
1418
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1419
+ },
1420
+ {
1421
+ "type": "github",
1422
+ "url": "https://github.com/sponsors/ai"
1423
+ }
1424
+ ],
1425
+ "license": "CC-BY-4.0"
1426
+ },
1427
+ "node_modules/clsx": {
1428
+ "version": "2.1.1",
1429
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1430
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1431
+ "license": "MIT",
1432
+ "engines": {
1433
+ "node": ">=6"
1434
+ }
1435
+ },
1436
+ "node_modules/convert-source-map": {
1437
+ "version": "2.0.0",
1438
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1439
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1440
+ "dev": true,
1441
+ "license": "MIT"
1442
+ },
1443
+ "node_modules/cookie": {
1444
+ "version": "1.1.1",
1445
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
1446
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
1447
+ "license": "MIT",
1448
+ "engines": {
1449
+ "node": ">=18"
1450
+ },
1451
+ "funding": {
1452
+ "type": "opencollective",
1453
+ "url": "https://opencollective.com/express"
1454
+ }
1455
+ },
1456
+ "node_modules/csstype": {
1457
+ "version": "3.2.3",
1458
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1459
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1460
+ "license": "MIT"
1461
+ },
1462
+ "node_modules/d3-array": {
1463
+ "version": "3.2.4",
1464
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
1465
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
1466
+ "license": "ISC",
1467
+ "dependencies": {
1468
+ "internmap": "1 - 2"
1469
+ },
1470
+ "engines": {
1471
+ "node": ">=12"
1472
+ }
1473
+ },
1474
+ "node_modules/d3-color": {
1475
+ "version": "3.1.0",
1476
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
1477
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
1478
+ "license": "ISC",
1479
+ "engines": {
1480
+ "node": ">=12"
1481
+ }
1482
+ },
1483
+ "node_modules/d3-ease": {
1484
+ "version": "3.0.1",
1485
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
1486
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
1487
+ "license": "BSD-3-Clause",
1488
+ "engines": {
1489
+ "node": ">=12"
1490
+ }
1491
+ },
1492
+ "node_modules/d3-format": {
1493
+ "version": "3.1.2",
1494
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
1495
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
1496
+ "license": "ISC",
1497
+ "engines": {
1498
+ "node": ">=12"
1499
+ }
1500
+ },
1501
+ "node_modules/d3-interpolate": {
1502
+ "version": "3.0.1",
1503
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
1504
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
1505
+ "license": "ISC",
1506
+ "dependencies": {
1507
+ "d3-color": "1 - 3"
1508
+ },
1509
+ "engines": {
1510
+ "node": ">=12"
1511
+ }
1512
+ },
1513
+ "node_modules/d3-path": {
1514
+ "version": "3.1.0",
1515
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
1516
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
1517
+ "license": "ISC",
1518
+ "engines": {
1519
+ "node": ">=12"
1520
+ }
1521
+ },
1522
+ "node_modules/d3-scale": {
1523
+ "version": "4.0.2",
1524
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
1525
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
1526
+ "license": "ISC",
1527
+ "dependencies": {
1528
+ "d3-array": "2.10.0 - 3",
1529
+ "d3-format": "1 - 3",
1530
+ "d3-interpolate": "1.2.0 - 3",
1531
+ "d3-time": "2.1.1 - 3",
1532
+ "d3-time-format": "2 - 4"
1533
+ },
1534
+ "engines": {
1535
+ "node": ">=12"
1536
+ }
1537
+ },
1538
+ "node_modules/d3-shape": {
1539
+ "version": "3.2.0",
1540
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
1541
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
1542
+ "license": "ISC",
1543
+ "dependencies": {
1544
+ "d3-path": "^3.1.0"
1545
+ },
1546
+ "engines": {
1547
+ "node": ">=12"
1548
+ }
1549
+ },
1550
+ "node_modules/d3-time": {
1551
+ "version": "3.1.0",
1552
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
1553
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
1554
+ "license": "ISC",
1555
+ "dependencies": {
1556
+ "d3-array": "2 - 3"
1557
+ },
1558
+ "engines": {
1559
+ "node": ">=12"
1560
+ }
1561
+ },
1562
+ "node_modules/d3-time-format": {
1563
+ "version": "4.1.0",
1564
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
1565
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
1566
+ "license": "ISC",
1567
+ "dependencies": {
1568
+ "d3-time": "1 - 3"
1569
+ },
1570
+ "engines": {
1571
+ "node": ">=12"
1572
+ }
1573
+ },
1574
+ "node_modules/d3-timer": {
1575
+ "version": "3.0.1",
1576
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
1577
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
1578
+ "license": "ISC",
1579
+ "engines": {
1580
+ "node": ">=12"
1581
+ }
1582
+ },
1583
+ "node_modules/debug": {
1584
+ "version": "4.4.3",
1585
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1586
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1587
+ "dev": true,
1588
+ "license": "MIT",
1589
+ "dependencies": {
1590
+ "ms": "^2.1.3"
1591
+ },
1592
+ "engines": {
1593
+ "node": ">=6.0"
1594
+ },
1595
+ "peerDependenciesMeta": {
1596
+ "supports-color": {
1597
+ "optional": true
1598
+ }
1599
+ }
1600
+ },
1601
+ "node_modules/decimal.js-light": {
1602
+ "version": "2.5.1",
1603
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
1604
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
1605
+ "license": "MIT"
1606
+ },
1607
+ "node_modules/dom-helpers": {
1608
+ "version": "5.2.1",
1609
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
1610
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
1611
+ "license": "MIT",
1612
+ "dependencies": {
1613
+ "@babel/runtime": "^7.8.7",
1614
+ "csstype": "^3.0.2"
1615
+ }
1616
+ },
1617
+ "node_modules/electron-to-chromium": {
1618
+ "version": "1.5.361",
1619
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
1620
+ "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==",
1621
+ "dev": true,
1622
+ "license": "ISC"
1623
+ },
1624
+ "node_modules/esbuild": {
1625
+ "version": "0.25.12",
1626
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1627
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1628
+ "dev": true,
1629
+ "hasInstallScript": true,
1630
+ "license": "MIT",
1631
+ "bin": {
1632
+ "esbuild": "bin/esbuild"
1633
+ },
1634
+ "engines": {
1635
+ "node": ">=18"
1636
+ },
1637
+ "optionalDependencies": {
1638
+ "@esbuild/aix-ppc64": "0.25.12",
1639
+ "@esbuild/android-arm": "0.25.12",
1640
+ "@esbuild/android-arm64": "0.25.12",
1641
+ "@esbuild/android-x64": "0.25.12",
1642
+ "@esbuild/darwin-arm64": "0.25.12",
1643
+ "@esbuild/darwin-x64": "0.25.12",
1644
+ "@esbuild/freebsd-arm64": "0.25.12",
1645
+ "@esbuild/freebsd-x64": "0.25.12",
1646
+ "@esbuild/linux-arm": "0.25.12",
1647
+ "@esbuild/linux-arm64": "0.25.12",
1648
+ "@esbuild/linux-ia32": "0.25.12",
1649
+ "@esbuild/linux-loong64": "0.25.12",
1650
+ "@esbuild/linux-mips64el": "0.25.12",
1651
+ "@esbuild/linux-ppc64": "0.25.12",
1652
+ "@esbuild/linux-riscv64": "0.25.12",
1653
+ "@esbuild/linux-s390x": "0.25.12",
1654
+ "@esbuild/linux-x64": "0.25.12",
1655
+ "@esbuild/netbsd-arm64": "0.25.12",
1656
+ "@esbuild/netbsd-x64": "0.25.12",
1657
+ "@esbuild/openbsd-arm64": "0.25.12",
1658
+ "@esbuild/openbsd-x64": "0.25.12",
1659
+ "@esbuild/openharmony-arm64": "0.25.12",
1660
+ "@esbuild/sunos-x64": "0.25.12",
1661
+ "@esbuild/win32-arm64": "0.25.12",
1662
+ "@esbuild/win32-ia32": "0.25.12",
1663
+ "@esbuild/win32-x64": "0.25.12"
1664
+ }
1665
+ },
1666
+ "node_modules/escalade": {
1667
+ "version": "3.2.0",
1668
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1669
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1670
+ "dev": true,
1671
+ "license": "MIT",
1672
+ "engines": {
1673
+ "node": ">=6"
1674
+ }
1675
+ },
1676
+ "node_modules/eventemitter3": {
1677
+ "version": "4.0.7",
1678
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
1679
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
1680
+ "license": "MIT"
1681
+ },
1682
+ "node_modules/fast-equals": {
1683
+ "version": "5.4.0",
1684
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
1685
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
1686
+ "license": "MIT",
1687
+ "engines": {
1688
+ "node": ">=6.0.0"
1689
+ }
1690
+ },
1691
+ "node_modules/fdir": {
1692
+ "version": "6.5.0",
1693
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1694
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1695
+ "dev": true,
1696
+ "license": "MIT",
1697
+ "engines": {
1698
+ "node": ">=12.0.0"
1699
+ },
1700
+ "peerDependencies": {
1701
+ "picomatch": "^3 || ^4"
1702
+ },
1703
+ "peerDependenciesMeta": {
1704
+ "picomatch": {
1705
+ "optional": true
1706
+ }
1707
+ }
1708
+ },
1709
+ "node_modules/fsevents": {
1710
+ "version": "2.3.3",
1711
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1712
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1713
+ "dev": true,
1714
+ "hasInstallScript": true,
1715
+ "license": "MIT",
1716
+ "optional": true,
1717
+ "os": [
1718
+ "darwin"
1719
+ ],
1720
+ "engines": {
1721
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1722
+ }
1723
+ },
1724
+ "node_modules/gensync": {
1725
+ "version": "1.0.0-beta.2",
1726
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1727
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1728
+ "dev": true,
1729
+ "license": "MIT",
1730
+ "engines": {
1731
+ "node": ">=6.9.0"
1732
+ }
1733
+ },
1734
+ "node_modules/internmap": {
1735
+ "version": "2.0.3",
1736
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
1737
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
1738
+ "license": "ISC",
1739
+ "engines": {
1740
+ "node": ">=12"
1741
+ }
1742
+ },
1743
+ "node_modules/js-tokens": {
1744
+ "version": "4.0.0",
1745
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1746
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1747
+ "license": "MIT"
1748
+ },
1749
+ "node_modules/jsesc": {
1750
+ "version": "3.1.0",
1751
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1752
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1753
+ "dev": true,
1754
+ "license": "MIT",
1755
+ "bin": {
1756
+ "jsesc": "bin/jsesc"
1757
+ },
1758
+ "engines": {
1759
+ "node": ">=6"
1760
+ }
1761
+ },
1762
+ "node_modules/json5": {
1763
+ "version": "2.2.3",
1764
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1765
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1766
+ "dev": true,
1767
+ "license": "MIT",
1768
+ "bin": {
1769
+ "json5": "lib/cli.js"
1770
+ },
1771
+ "engines": {
1772
+ "node": ">=6"
1773
+ }
1774
+ },
1775
+ "node_modules/lodash": {
1776
+ "version": "4.18.1",
1777
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
1778
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
1779
+ "license": "MIT"
1780
+ },
1781
+ "node_modules/loose-envify": {
1782
+ "version": "1.4.0",
1783
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1784
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1785
+ "license": "MIT",
1786
+ "dependencies": {
1787
+ "js-tokens": "^3.0.0 || ^4.0.0"
1788
+ },
1789
+ "bin": {
1790
+ "loose-envify": "cli.js"
1791
+ }
1792
+ },
1793
+ "node_modules/lru-cache": {
1794
+ "version": "5.1.1",
1795
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1796
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1797
+ "dev": true,
1798
+ "license": "ISC",
1799
+ "dependencies": {
1800
+ "yallist": "^3.0.2"
1801
+ }
1802
+ },
1803
+ "node_modules/ms": {
1804
+ "version": "2.1.3",
1805
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1806
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1807
+ "dev": true,
1808
+ "license": "MIT"
1809
+ },
1810
+ "node_modules/nanoid": {
1811
+ "version": "3.3.12",
1812
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
1813
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
1814
+ "dev": true,
1815
+ "funding": [
1816
+ {
1817
+ "type": "github",
1818
+ "url": "https://github.com/sponsors/ai"
1819
+ }
1820
+ ],
1821
+ "license": "MIT",
1822
+ "bin": {
1823
+ "nanoid": "bin/nanoid.cjs"
1824
+ },
1825
+ "engines": {
1826
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1827
+ }
1828
+ },
1829
+ "node_modules/node-releases": {
1830
+ "version": "2.0.46",
1831
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
1832
+ "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
1833
+ "dev": true,
1834
+ "license": "MIT",
1835
+ "engines": {
1836
+ "node": ">=18"
1837
+ }
1838
+ },
1839
+ "node_modules/object-assign": {
1840
+ "version": "4.1.1",
1841
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1842
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1843
+ "license": "MIT",
1844
+ "engines": {
1845
+ "node": ">=0.10.0"
1846
+ }
1847
+ },
1848
+ "node_modules/picocolors": {
1849
+ "version": "1.1.1",
1850
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1851
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1852
+ "dev": true,
1853
+ "license": "ISC"
1854
+ },
1855
+ "node_modules/picomatch": {
1856
+ "version": "4.0.4",
1857
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1858
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1859
+ "dev": true,
1860
+ "license": "MIT",
1861
+ "engines": {
1862
+ "node": ">=12"
1863
+ },
1864
+ "funding": {
1865
+ "url": "https://github.com/sponsors/jonschlinkert"
1866
+ }
1867
+ },
1868
+ "node_modules/postcss": {
1869
+ "version": "8.5.15",
1870
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
1871
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
1872
+ "dev": true,
1873
+ "funding": [
1874
+ {
1875
+ "type": "opencollective",
1876
+ "url": "https://opencollective.com/postcss/"
1877
+ },
1878
+ {
1879
+ "type": "tidelift",
1880
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1881
+ },
1882
+ {
1883
+ "type": "github",
1884
+ "url": "https://github.com/sponsors/ai"
1885
+ }
1886
+ ],
1887
+ "license": "MIT",
1888
+ "dependencies": {
1889
+ "nanoid": "^3.3.12",
1890
+ "picocolors": "^1.1.1",
1891
+ "source-map-js": "^1.2.1"
1892
+ },
1893
+ "engines": {
1894
+ "node": "^10 || ^12 || >=14"
1895
+ }
1896
+ },
1897
+ "node_modules/prop-types": {
1898
+ "version": "15.8.1",
1899
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
1900
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
1901
+ "license": "MIT",
1902
+ "dependencies": {
1903
+ "loose-envify": "^1.4.0",
1904
+ "object-assign": "^4.1.1",
1905
+ "react-is": "^16.13.1"
1906
+ }
1907
+ },
1908
+ "node_modules/prop-types/node_modules/react-is": {
1909
+ "version": "16.13.1",
1910
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
1911
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
1912
+ "license": "MIT"
1913
+ },
1914
+ "node_modules/react": {
1915
+ "version": "19.2.6",
1916
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
1917
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
1918
+ "license": "MIT",
1919
+ "engines": {
1920
+ "node": ">=0.10.0"
1921
+ }
1922
+ },
1923
+ "node_modules/react-dom": {
1924
+ "version": "19.2.6",
1925
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
1926
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
1927
+ "license": "MIT",
1928
+ "dependencies": {
1929
+ "scheduler": "^0.27.0"
1930
+ },
1931
+ "peerDependencies": {
1932
+ "react": "^19.2.6"
1933
+ }
1934
+ },
1935
+ "node_modules/react-is": {
1936
+ "version": "18.3.1",
1937
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
1938
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
1939
+ "license": "MIT"
1940
+ },
1941
+ "node_modules/react-refresh": {
1942
+ "version": "0.17.0",
1943
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1944
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1945
+ "dev": true,
1946
+ "license": "MIT",
1947
+ "engines": {
1948
+ "node": ">=0.10.0"
1949
+ }
1950
+ },
1951
+ "node_modules/react-router": {
1952
+ "version": "7.15.1",
1953
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
1954
+ "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
1955
+ "license": "MIT",
1956
+ "dependencies": {
1957
+ "cookie": "^1.0.1",
1958
+ "set-cookie-parser": "^2.6.0"
1959
+ },
1960
+ "engines": {
1961
+ "node": ">=20.0.0"
1962
+ },
1963
+ "peerDependencies": {
1964
+ "react": ">=18",
1965
+ "react-dom": ">=18"
1966
+ },
1967
+ "peerDependenciesMeta": {
1968
+ "react-dom": {
1969
+ "optional": true
1970
+ }
1971
+ }
1972
+ },
1973
+ "node_modules/react-router-dom": {
1974
+ "version": "7.15.1",
1975
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
1976
+ "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
1977
+ "license": "MIT",
1978
+ "dependencies": {
1979
+ "react-router": "7.15.1"
1980
+ },
1981
+ "engines": {
1982
+ "node": ">=20.0.0"
1983
+ },
1984
+ "peerDependencies": {
1985
+ "react": ">=18",
1986
+ "react-dom": ">=18"
1987
+ }
1988
+ },
1989
+ "node_modules/react-smooth": {
1990
+ "version": "4.0.4",
1991
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
1992
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
1993
+ "license": "MIT",
1994
+ "dependencies": {
1995
+ "fast-equals": "^5.0.1",
1996
+ "prop-types": "^15.8.1",
1997
+ "react-transition-group": "^4.4.5"
1998
+ },
1999
+ "peerDependencies": {
2000
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2001
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2002
+ }
2003
+ },
2004
+ "node_modules/react-transition-group": {
2005
+ "version": "4.4.5",
2006
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
2007
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
2008
+ "license": "BSD-3-Clause",
2009
+ "dependencies": {
2010
+ "@babel/runtime": "^7.5.5",
2011
+ "dom-helpers": "^5.0.1",
2012
+ "loose-envify": "^1.4.0",
2013
+ "prop-types": "^15.6.2"
2014
+ },
2015
+ "peerDependencies": {
2016
+ "react": ">=16.6.0",
2017
+ "react-dom": ">=16.6.0"
2018
+ }
2019
+ },
2020
+ "node_modules/recharts": {
2021
+ "version": "2.15.4",
2022
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
2023
+ "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
2024
+ "license": "MIT",
2025
+ "dependencies": {
2026
+ "clsx": "^2.0.0",
2027
+ "eventemitter3": "^4.0.1",
2028
+ "lodash": "^4.17.21",
2029
+ "react-is": "^18.3.1",
2030
+ "react-smooth": "^4.0.4",
2031
+ "recharts-scale": "^0.4.4",
2032
+ "tiny-invariant": "^1.3.1",
2033
+ "victory-vendor": "^36.6.8"
2034
+ },
2035
+ "engines": {
2036
+ "node": ">=14"
2037
+ },
2038
+ "peerDependencies": {
2039
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
2040
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
2041
+ }
2042
+ },
2043
+ "node_modules/recharts-scale": {
2044
+ "version": "0.4.5",
2045
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
2046
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
2047
+ "license": "MIT",
2048
+ "dependencies": {
2049
+ "decimal.js-light": "^2.4.1"
2050
+ }
2051
+ },
2052
+ "node_modules/rollup": {
2053
+ "version": "4.60.4",
2054
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
2055
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
2056
+ "dev": true,
2057
+ "license": "MIT",
2058
+ "dependencies": {
2059
+ "@types/estree": "1.0.8"
2060
+ },
2061
+ "bin": {
2062
+ "rollup": "dist/bin/rollup"
2063
+ },
2064
+ "engines": {
2065
+ "node": ">=18.0.0",
2066
+ "npm": ">=8.0.0"
2067
+ },
2068
+ "optionalDependencies": {
2069
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
2070
+ "@rollup/rollup-android-arm64": "4.60.4",
2071
+ "@rollup/rollup-darwin-arm64": "4.60.4",
2072
+ "@rollup/rollup-darwin-x64": "4.60.4",
2073
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
2074
+ "@rollup/rollup-freebsd-x64": "4.60.4",
2075
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
2076
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
2077
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
2078
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
2079
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
2080
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
2081
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
2082
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
2083
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
2084
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
2085
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
2086
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
2087
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
2088
+ "@rollup/rollup-openbsd-x64": "4.60.4",
2089
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
2090
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
2091
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
2092
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
2093
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
2094
+ "fsevents": "~2.3.2"
2095
+ }
2096
+ },
2097
+ "node_modules/scheduler": {
2098
+ "version": "0.27.0",
2099
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2100
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2101
+ "license": "MIT"
2102
+ },
2103
+ "node_modules/semver": {
2104
+ "version": "6.3.1",
2105
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2106
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2107
+ "dev": true,
2108
+ "license": "ISC",
2109
+ "bin": {
2110
+ "semver": "bin/semver.js"
2111
+ }
2112
+ },
2113
+ "node_modules/set-cookie-parser": {
2114
+ "version": "2.7.2",
2115
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
2116
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
2117
+ "license": "MIT"
2118
+ },
2119
+ "node_modules/source-map-js": {
2120
+ "version": "1.2.1",
2121
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2122
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2123
+ "dev": true,
2124
+ "license": "BSD-3-Clause",
2125
+ "engines": {
2126
+ "node": ">=0.10.0"
2127
+ }
2128
+ },
2129
+ "node_modules/tiny-invariant": {
2130
+ "version": "1.3.3",
2131
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
2132
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
2133
+ "license": "MIT"
2134
+ },
2135
+ "node_modules/tinyglobby": {
2136
+ "version": "0.2.16",
2137
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2138
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2139
+ "dev": true,
2140
+ "license": "MIT",
2141
+ "dependencies": {
2142
+ "fdir": "^6.5.0",
2143
+ "picomatch": "^4.0.4"
2144
+ },
2145
+ "engines": {
2146
+ "node": ">=12.0.0"
2147
+ },
2148
+ "funding": {
2149
+ "url": "https://github.com/sponsors/SuperchupuDev"
2150
+ }
2151
+ },
2152
+ "node_modules/typescript": {
2153
+ "version": "5.7.3",
2154
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
2155
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
2156
+ "dev": true,
2157
+ "license": "Apache-2.0",
2158
+ "bin": {
2159
+ "tsc": "bin/tsc",
2160
+ "tsserver": "bin/tsserver"
2161
+ },
2162
+ "engines": {
2163
+ "node": ">=14.17"
2164
+ }
2165
+ },
2166
+ "node_modules/update-browserslist-db": {
2167
+ "version": "1.2.3",
2168
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2169
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2170
+ "dev": true,
2171
+ "funding": [
2172
+ {
2173
+ "type": "opencollective",
2174
+ "url": "https://opencollective.com/browserslist"
2175
+ },
2176
+ {
2177
+ "type": "tidelift",
2178
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2179
+ },
2180
+ {
2181
+ "type": "github",
2182
+ "url": "https://github.com/sponsors/ai"
2183
+ }
2184
+ ],
2185
+ "license": "MIT",
2186
+ "dependencies": {
2187
+ "escalade": "^3.2.0",
2188
+ "picocolors": "^1.1.1"
2189
+ },
2190
+ "bin": {
2191
+ "update-browserslist-db": "cli.js"
2192
+ },
2193
+ "peerDependencies": {
2194
+ "browserslist": ">= 4.21.0"
2195
+ }
2196
+ },
2197
+ "node_modules/victory-vendor": {
2198
+ "version": "36.9.2",
2199
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
2200
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
2201
+ "license": "MIT AND ISC",
2202
+ "dependencies": {
2203
+ "@types/d3-array": "^3.0.3",
2204
+ "@types/d3-ease": "^3.0.0",
2205
+ "@types/d3-interpolate": "^3.0.1",
2206
+ "@types/d3-scale": "^4.0.2",
2207
+ "@types/d3-shape": "^3.1.0",
2208
+ "@types/d3-time": "^3.0.0",
2209
+ "@types/d3-timer": "^3.0.0",
2210
+ "d3-array": "^3.1.6",
2211
+ "d3-ease": "^3.0.1",
2212
+ "d3-interpolate": "^3.0.1",
2213
+ "d3-scale": "^4.0.2",
2214
+ "d3-shape": "^3.1.0",
2215
+ "d3-time": "^3.0.0",
2216
+ "d3-timer": "^3.0.1"
2217
+ }
2218
+ },
2219
+ "node_modules/vite": {
2220
+ "version": "6.4.2",
2221
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
2222
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
2223
+ "dev": true,
2224
+ "license": "MIT",
2225
+ "dependencies": {
2226
+ "esbuild": "^0.25.0",
2227
+ "fdir": "^6.4.4",
2228
+ "picomatch": "^4.0.2",
2229
+ "postcss": "^8.5.3",
2230
+ "rollup": "^4.34.9",
2231
+ "tinyglobby": "^0.2.13"
2232
+ },
2233
+ "bin": {
2234
+ "vite": "bin/vite.js"
2235
+ },
2236
+ "engines": {
2237
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2238
+ },
2239
+ "funding": {
2240
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2241
+ },
2242
+ "optionalDependencies": {
2243
+ "fsevents": "~2.3.3"
2244
+ },
2245
+ "peerDependencies": {
2246
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2247
+ "jiti": ">=1.21.0",
2248
+ "less": "*",
2249
+ "lightningcss": "^1.21.0",
2250
+ "sass": "*",
2251
+ "sass-embedded": "*",
2252
+ "stylus": "*",
2253
+ "sugarss": "*",
2254
+ "terser": "^5.16.0",
2255
+ "tsx": "^4.8.1",
2256
+ "yaml": "^2.4.2"
2257
+ },
2258
+ "peerDependenciesMeta": {
2259
+ "@types/node": {
2260
+ "optional": true
2261
+ },
2262
+ "jiti": {
2263
+ "optional": true
2264
+ },
2265
+ "less": {
2266
+ "optional": true
2267
+ },
2268
+ "lightningcss": {
2269
+ "optional": true
2270
+ },
2271
+ "sass": {
2272
+ "optional": true
2273
+ },
2274
+ "sass-embedded": {
2275
+ "optional": true
2276
+ },
2277
+ "stylus": {
2278
+ "optional": true
2279
+ },
2280
+ "sugarss": {
2281
+ "optional": true
2282
+ },
2283
+ "terser": {
2284
+ "optional": true
2285
+ },
2286
+ "tsx": {
2287
+ "optional": true
2288
+ },
2289
+ "yaml": {
2290
+ "optional": true
2291
+ }
2292
+ }
2293
+ },
2294
+ "node_modules/yallist": {
2295
+ "version": "3.1.1",
2296
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2297
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2298
+ "dev": true,
2299
+ "license": "ISC"
2300
+ }
2301
+ }
2302
+ }
frontend/package.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "youtube_hate_detector-ui",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^19.0.0",
13
+ "react-dom": "^19.0.0",
14
+ "react-router-dom": "^7.1.0",
15
+ "recharts": "^2.15.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "@vitejs/plugin-react": "^4.3.4",
21
+ "typescript": "~5.7.2",
22
+ "vite": "^6.0.0"
23
+ }
24
+ }
frontend/src/App.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter, Route, Routes } from "react-router-dom";
2
+ import { Layout } from "./components/Layout";
3
+ import { AppProvider } from "./context/AppContext";
4
+ import { HubPage } from "./pages/HubPage";
5
+ import { SettingsPage } from "./pages/SettingsPage";
6
+ import { WatchPage } from "./pages/WatchPage";
7
+
8
+ export default function App() {
9
+ return (
10
+ <AppProvider>
11
+ <BrowserRouter>
12
+ <Routes>
13
+ <Route element={<Layout />}>
14
+ <Route index element={<WatchPage />} />
15
+ <Route path="hub" element={<HubPage />} />
16
+ <Route path="settings" element={<SettingsPage />} />
17
+ </Route>
18
+ </Routes>
19
+ </BrowserRouter>
20
+ </AppProvider>
21
+ );
22
+ }
frontend/src/api/client.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ ModelStatusEntry,
3
+ PredictResponse,
4
+ SuggestedVideo,
5
+ VideoResponse,
6
+ } from "../types/api";
7
+
8
+ const BASE = import.meta.env.VITE_API_BASE_URL ?? "";
9
+
10
+ function parseApiError(body: unknown, fallback: string): string {
11
+ if (!body || typeof body !== "object") return fallback;
12
+ const detail = (body as { detail?: unknown }).detail;
13
+ if (typeof detail === "string") return detail;
14
+ if (Array.isArray(detail)) {
15
+ return detail
16
+ .map((item) => {
17
+ if (item && typeof item === "object" && "msg" in item) {
18
+ return String((item as { msg: unknown }).msg);
19
+ }
20
+ return String(item);
21
+ })
22
+ .join("; ");
23
+ }
24
+ return fallback;
25
+ }
26
+
27
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
28
+ const res = await fetch(`${BASE}${path}`, {
29
+ ...init,
30
+ headers: { "Content-Type": "application/json", ...init?.headers },
31
+ });
32
+ if (!res.ok) {
33
+ const err = await res.json().catch(() => ({}));
34
+ throw new Error(parseApiError(err, res.statusText));
35
+ }
36
+ return res.json() as Promise<T>;
37
+ }
38
+
39
+ export function predict(text: string, threshold: number) {
40
+ return request<PredictResponse>("/predict", {
41
+ method: "POST",
42
+ body: JSON.stringify({ text, threshold }),
43
+ });
44
+ }
45
+
46
+ export function predictVideo(url: string, maxComments: number, threshold: number) {
47
+ return request<VideoResponse>("/predict-video", {
48
+ method: "POST",
49
+ body: JSON.stringify({ url, max_comments: maxComments, threshold }),
50
+ });
51
+ }
52
+
53
+ export function getModels() {
54
+ return request<{ available: string[]; active: string }>("/models");
55
+ }
56
+
57
+ export function getModelsStatus() {
58
+ return request<{ models: ModelStatusEntry[]; active: string }>("/models/status");
59
+ }
60
+
61
+ export function setModel(name: string) {
62
+ return request<{ message: string; model: string }>(`/model/${encodeURIComponent(name)}`, {
63
+ method: "PUT",
64
+ });
65
+ }
66
+
67
+ export function getSuggestedVideos() {
68
+ return request<{ videos: SuggestedVideo[]; max_comments: number }>("/videos/suggested");
69
+ }
70
+
71
+ export function getModelInfo() {
72
+ return request<{
73
+ name: string;
74
+ description: string;
75
+ predictions_served: number;
76
+ }>("/model-info");
77
+ }
frontend/src/components/CommentRow.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { formatPct, toxicityColor } from "../utils/toxicity";
2
+ import type { CommentItem } from "../types/api";
3
+
4
+ type Props = { comment: CommentItem };
5
+
6
+ export function CommentRow({ comment }: Props) {
7
+ const color = toxicityColor(comment.probability);
8
+ return (
9
+ <div className={`comment-row ${comment.is_toxic ? "toxic" : "safe"}`}>
10
+ <div className={`avatar ${comment.is_toxic ? "toxic" : ""}`}>
11
+ {comment.user.charAt(0).toUpperCase()}
12
+ </div>
13
+ <div className="comment-body">
14
+ <div className="comment-meta">
15
+ <span className="comment-user">@{comment.user}</span>
16
+ <span className="comment-time">{comment.time}</span>
17
+ <span className={`badge ${comment.is_toxic ? "badge-toxic" : "badge-safe"}`}>
18
+ {comment.is_toxic ? "Toxic" : "Safe"}
19
+ </span>
20
+ <span className="comment-pct" style={{ color }}>
21
+ {formatPct(comment.probability)}
22
+ </span>
23
+ </div>
24
+ <p className="comment-text">{comment.text}</p>
25
+ {comment.is_toxic && <p className="flagged">Flagged for review</p>}
26
+ {comment.labels.length > 0 && (
27
+ <p className="comment-labels">{comment.labels.join(" · ")}</p>
28
+ )}
29
+ </div>
30
+ </div>
31
+ );
32
+ }
frontend/src/components/Layout.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NavLink, Outlet } from "react-router-dom";
2
+
3
+ export function Layout() {
4
+ return (
5
+ <div className="app-shell">
6
+ <nav className="sidebar">
7
+ <div className="logo">youtube_hate_detector</div>
8
+ <NavLink to="/" end className={({ isActive }) => (isActive ? "nav active" : "nav")}>
9
+ Watch
10
+ </NavLink>
11
+ <NavLink to="/hub" className={({ isActive }) => (isActive ? "nav active" : "nav")}>
12
+ Moderator Hub
13
+ </NavLink>
14
+ <NavLink to="/settings" className={({ isActive }) => (isActive ? "nav active" : "nav")}>
15
+ Settings
16
+ </NavLink>
17
+ </nav>
18
+ <main className="main-content">
19
+ <Outlet />
20
+ </main>
21
+ </div>
22
+ );
23
+ }
frontend/src/components/SuggestedRail.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { SuggestedVideo } from "../types/api";
2
+
3
+ type Props = {
4
+ videos: SuggestedVideo[];
5
+ activeId: string | null;
6
+ loadingId: string | null;
7
+ onSelect: (video: SuggestedVideo) => void;
8
+ };
9
+
10
+ export function SuggestedRail({ videos, activeId, loadingId, onSelect }: Props) {
11
+ return (
12
+ <aside className="suggested-rail">
13
+ <p className="rail-title">Up next</p>
14
+ {videos.map((v) => (
15
+ <button
16
+ key={v.id}
17
+ type="button"
18
+ className={`suggested-card ${activeId === v.id ? "active" : ""}`}
19
+ onClick={() => onSelect(v)}
20
+ disabled={loadingId === v.id}
21
+ >
22
+ <img src={v.thumbnail_url} alt="" className="suggested-thumb" />
23
+ <div className="suggested-info">
24
+ <p className="suggested-title">{v.title}</p>
25
+ <p className="suggested-channel">{v.channel_title}</p>
26
+ {!v.embeddable && <span className="embed-badge">External only</span>}
27
+ {loadingId === v.id && <span className="loading-tag">Loading comments…</span>}
28
+ </div>
29
+ </button>
30
+ ))}
31
+ </aside>
32
+ );
33
+ }
frontend/src/context/AppContext.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ type ReactNode,
8
+ } from "react";
9
+
10
+ export type HubEntry = {
11
+ user: string;
12
+ snippet: string;
13
+ score: number;
14
+ action: string;
15
+ };
16
+
17
+ type AppContextValue = {
18
+ threshold: number;
19
+ setThreshold: (v: number) => void;
20
+ hubHistory: HubEntry[];
21
+ addHubEntry: (entry: HubEntry) => void;
22
+ };
23
+
24
+ const AppContext = createContext<AppContextValue | null>(null);
25
+
26
+ export function AppProvider({ children }: { children: ReactNode }) {
27
+ const [threshold, setThreshold] = useState(0.5);
28
+ const [hubHistory, setHubHistory] = useState<HubEntry[]>([]);
29
+
30
+ const addHubEntry = useCallback((entry: HubEntry) => {
31
+ setHubHistory((prev) => [entry, ...prev].slice(0, 100));
32
+ }, []);
33
+
34
+ const value = useMemo(
35
+ () => ({
36
+ threshold,
37
+ setThreshold,
38
+ hubHistory,
39
+ addHubEntry,
40
+ }),
41
+ [threshold, hubHistory, addHubEntry]
42
+ );
43
+
44
+ return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
45
+ }
46
+
47
+ export function useApp() {
48
+ const ctx = useContext(AppContext);
49
+ if (!ctx) throw new Error("useApp must be used within AppProvider");
50
+ return ctx;
51
+ }
frontend/src/hooks/useDebouncedPredict.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { predict } from "../api/client";
3
+ import type { PredictResponse } from "../types/api";
4
+
5
+ export function useDebouncedPredict(text: string, threshold: number, delayMs = 400) {
6
+ const [result, setResult] = useState<PredictResponse | null>(null);
7
+ const [loading, setLoading] = useState(false);
8
+ const [error, setError] = useState<string | null>(null);
9
+ const abortRef = useRef<AbortController | null>(null);
10
+
11
+ useEffect(() => {
12
+ const trimmed = text.trim();
13
+ if (!trimmed) {
14
+ setResult(null);
15
+ setError(null);
16
+ setLoading(false);
17
+ return;
18
+ }
19
+
20
+ const timer = setTimeout(() => {
21
+ abortRef.current?.abort();
22
+ setLoading(true);
23
+ setError(null);
24
+ predict(trimmed, threshold)
25
+ .then(setResult)
26
+ .catch((e: Error) => setError(e.message))
27
+ .finally(() => setLoading(false));
28
+ }, delayMs);
29
+
30
+ return () => clearTimeout(timer);
31
+ }, [text, threshold, delayMs]);
32
+
33
+ return { result, loading, error };
34
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --yt-bg: #0f0f0f;
3
+ --yt-surface: #212121;
4
+ --yt-text: #f1f1f1;
5
+ --yt-muted: #aaaaaa;
6
+ --yt-accent: #3ea6ff;
7
+ --yt-red: #ff4e45;
8
+ --yt-green: #2ba640;
9
+ font-family: Roboto, Arial, sans-serif;
10
+ color: var(--yt-text);
11
+ background: var(--yt-bg);
12
+ }
13
+
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ margin: 0;
20
+ min-height: 100vh;
21
+ }
22
+
23
+ .app-shell {
24
+ display: flex;
25
+ min-height: 100vh;
26
+ }
27
+
28
+ .sidebar {
29
+ width: 240px;
30
+ background: #0f0f0f;
31
+ border-right: 1px solid #303030;
32
+ padding: 1rem 0.75rem;
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 0.25rem;
36
+ flex-shrink: 0;
37
+ }
38
+
39
+ .logo {
40
+ font-weight: 700;
41
+ font-size: 0.95rem;
42
+ padding: 0.5rem 0.75rem 1rem;
43
+ color: #fff;
44
+ word-break: break-word;
45
+ }
46
+
47
+ .nav {
48
+ color: var(--yt-muted);
49
+ text-decoration: none;
50
+ padding: 0.6rem 0.75rem;
51
+ border-radius: 10px;
52
+ font-size: 0.9rem;
53
+ }
54
+
55
+ .nav:hover,
56
+ .nav.active {
57
+ background: #272727;
58
+ color: #fff;
59
+ }
60
+
61
+ .main-content {
62
+ flex: 1;
63
+ overflow-x: hidden;
64
+ }
65
+
66
+ .watch-page {
67
+ padding: 1.5rem 1.5rem 3rem;
68
+ }
69
+
70
+ .watch-grid {
71
+ display: grid;
72
+ grid-template-columns: 1fr 400px;
73
+ gap: 1.5rem;
74
+ align-items: start;
75
+ }
76
+
77
+ @media (max-width: 1100px) {
78
+ .watch-grid {
79
+ grid-template-columns: 1fr;
80
+ }
81
+ }
82
+
83
+ .staged-player {
84
+ width: 100%;
85
+ aspect-ratio: 16 / 9;
86
+ background: #000;
87
+ border-radius: 12px;
88
+ overflow: hidden;
89
+ }
90
+
91
+ .player-poster {
92
+ width: 100%;
93
+ height: 100%;
94
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
95
+ display: flex;
96
+ flex-direction: column;
97
+ align-items: center;
98
+ justify-content: center;
99
+ gap: 0.75rem;
100
+ }
101
+
102
+ .player-hint {
103
+ color: var(--yt-muted);
104
+ font-size: 0.9rem;
105
+ margin: 0;
106
+ }
107
+
108
+ .player-iframe {
109
+ width: 100%;
110
+ height: 100%;
111
+ border: 0;
112
+ display: block;
113
+ }
114
+
115
+ .player-fallback {
116
+ display: flex;
117
+ flex-direction: column;
118
+ align-items: center;
119
+ justify-content: center;
120
+ width: 100%;
121
+ height: 100%;
122
+ text-decoration: none;
123
+ color: inherit;
124
+ position: relative;
125
+ background: #000;
126
+ }
127
+
128
+ .player-fallback-thumb {
129
+ width: 100%;
130
+ height: 100%;
131
+ object-fit: cover;
132
+ opacity: 0.75;
133
+ }
134
+
135
+ .player-fallback-cta {
136
+ position: absolute;
137
+ bottom: 1rem;
138
+ background: rgba(0, 0, 0, 0.85);
139
+ padding: 0.5rem 1rem;
140
+ border-radius: 8px;
141
+ font-size: 0.9rem;
142
+ color: var(--yt-accent);
143
+ }
144
+
145
+ .play-icon {
146
+ width: 68px;
147
+ height: 48px;
148
+ background: rgba(255, 255, 255, 0.9);
149
+ color: #000;
150
+ border-radius: 12px;
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ font-size: 1.25rem;
155
+ }
156
+
157
+ .video-title {
158
+ font-size: 1.25rem;
159
+ font-weight: 600;
160
+ margin: 0.75rem 0 0.35rem;
161
+ line-height: 1.4;
162
+ word-break: break-word;
163
+ display: -webkit-box;
164
+ -webkit-line-clamp: 3;
165
+ -webkit-box-orient: vertical;
166
+ overflow: hidden;
167
+ }
168
+
169
+ .video-meta {
170
+ color: var(--yt-muted);
171
+ font-size: 0.85rem;
172
+ margin: 0;
173
+ }
174
+
175
+ .channel-row {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 0.75rem;
179
+ margin: 1rem 0;
180
+ padding-bottom: 1rem;
181
+ border-bottom: 1px solid #303030;
182
+ }
183
+
184
+ .channel-avatar {
185
+ width: 40px;
186
+ height: 40px;
187
+ border-radius: 50%;
188
+ background: var(--yt-red);
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: center;
192
+ font-weight: 700;
193
+ }
194
+
195
+ .channel-name {
196
+ margin: 0;
197
+ font-weight: 500;
198
+ }
199
+
200
+ .info-banner {
201
+ font-size: 0.85rem;
202
+ color: var(--yt-muted);
203
+ background: #1a2a3a;
204
+ border: 1px solid #303030;
205
+ border-radius: 8px;
206
+ padding: 0.6rem 0.85rem;
207
+ margin: 0.5rem 0 0.75rem;
208
+ }
209
+
210
+ .info-banner.dismissible {
211
+ display: flex;
212
+ align-items: flex-start;
213
+ justify-content: space-between;
214
+ gap: 0.75rem;
215
+ color: var(--yt-text);
216
+ }
217
+
218
+ .btn-dismiss {
219
+ background: transparent;
220
+ border: none;
221
+ color: var(--yt-muted);
222
+ font-size: 1.25rem;
223
+ line-height: 1;
224
+ cursor: pointer;
225
+ padding: 0 0.25rem;
226
+ }
227
+
228
+ .btn-dismiss:hover {
229
+ color: var(--yt-text);
230
+ }
231
+
232
+ .loading-comments {
233
+ color: var(--yt-muted);
234
+ font-size: 0.9rem;
235
+ margin: 0.5rem 0;
236
+ }
237
+
238
+ .comments-header {
239
+ font-size: 1rem;
240
+ font-weight: 600;
241
+ margin-bottom: 1rem;
242
+ }
243
+
244
+ .toxic-count {
245
+ color: var(--yt-red);
246
+ font-weight: 500;
247
+ font-size: 0.85rem;
248
+ }
249
+
250
+ .comment-compose textarea {
251
+ width: 100%;
252
+ background: transparent;
253
+ border: none;
254
+ border-bottom: 1px solid #303030;
255
+ color: var(--yt-text);
256
+ font-size: 0.95rem;
257
+ resize: vertical;
258
+ padding: 0.5rem 0;
259
+ font-family: inherit;
260
+ }
261
+
262
+ .comment-compose textarea:focus {
263
+ outline: none;
264
+ border-bottom-color: var(--yt-accent);
265
+ }
266
+
267
+ .live-analysis {
268
+ display: flex;
269
+ flex-wrap: wrap;
270
+ gap: 0.5rem 1rem;
271
+ align-items: center;
272
+ font-size: 0.85rem;
273
+ margin: 0.5rem 0;
274
+ color: var(--yt-muted);
275
+ }
276
+
277
+ .compose-actions {
278
+ display: flex;
279
+ justify-content: flex-end;
280
+ gap: 0.5rem;
281
+ margin-top: 0.75rem;
282
+ }
283
+
284
+ .btn-primary,
285
+ .btn-secondary,
286
+ .btn-danger {
287
+ border: none;
288
+ border-radius: 999px;
289
+ padding: 0.5rem 1rem;
290
+ font-weight: 500;
291
+ cursor: pointer;
292
+ font-size: 0.9rem;
293
+ }
294
+
295
+ .btn-primary {
296
+ background: #3f3f3f;
297
+ color: #fff;
298
+ }
299
+
300
+ .btn-primary:hover {
301
+ background: #525252;
302
+ }
303
+
304
+ .btn-secondary {
305
+ background: transparent;
306
+ color: var(--yt-accent);
307
+ }
308
+
309
+ .btn-danger {
310
+ background: var(--yt-red);
311
+ color: #fff;
312
+ }
313
+
314
+ .badge {
315
+ font-size: 0.7rem;
316
+ font-weight: 700;
317
+ padding: 2px 8px;
318
+ border-radius: 4px;
319
+ text-transform: uppercase;
320
+ }
321
+
322
+ .badge-safe {
323
+ background: rgba(43, 166, 64, 0.2);
324
+ color: var(--yt-green);
325
+ }
326
+
327
+ .badge-toxic {
328
+ background: rgba(255, 78, 69, 0.2);
329
+ color: var(--yt-red);
330
+ }
331
+
332
+ .comment-list {
333
+ margin-top: 1.5rem;
334
+ }
335
+
336
+ .comment-row {
337
+ display: flex;
338
+ gap: 0.75rem;
339
+ padding: 0.75rem 0;
340
+ border-bottom: 1px solid #222;
341
+ }
342
+
343
+ .avatar {
344
+ width: 40px;
345
+ height: 40px;
346
+ border-radius: 50%;
347
+ background: #717171;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ font-weight: 600;
352
+ flex-shrink: 0;
353
+ }
354
+
355
+ .avatar.toxic {
356
+ background: #5c1a1a;
357
+ }
358
+
359
+ .comment-meta {
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ gap: 0.35rem 0.75rem;
363
+ align-items: center;
364
+ font-size: 0.8rem;
365
+ }
366
+
367
+ .comment-user {
368
+ font-weight: 500;
369
+ color: var(--yt-text);
370
+ }
371
+
372
+ .comment-time,
373
+ .comment-pct {
374
+ color: var(--yt-muted);
375
+ }
376
+
377
+ .comment-text {
378
+ margin: 0.35rem 0 0;
379
+ line-height: 1.45;
380
+ font-size: 0.9rem;
381
+ }
382
+
383
+ .flagged {
384
+ color: var(--yt-red);
385
+ font-size: 0.8rem;
386
+ margin: 0.25rem 0 0;
387
+ }
388
+
389
+ .comment-labels {
390
+ font-size: 0.75rem;
391
+ color: var(--yt-muted);
392
+ margin: 0.2rem 0 0;
393
+ }
394
+
395
+ .suggested-rail {
396
+ display: flex;
397
+ flex-direction: column;
398
+ gap: 0.5rem;
399
+ }
400
+
401
+ .rail-title {
402
+ font-weight: 600;
403
+ margin: 0 0 0.5rem;
404
+ }
405
+
406
+ .suggested-card {
407
+ display: flex;
408
+ gap: 0.5rem;
409
+ background: transparent;
410
+ border: none;
411
+ color: inherit;
412
+ text-align: left;
413
+ cursor: pointer;
414
+ padding: 0.25rem;
415
+ border-radius: 8px;
416
+ width: 100%;
417
+ }
418
+
419
+ .suggested-card:hover,
420
+ .suggested-card.active {
421
+ background: #272727;
422
+ }
423
+
424
+ .suggested-thumb {
425
+ width: 168px;
426
+ height: 94px;
427
+ object-fit: cover;
428
+ border-radius: 8px;
429
+ flex-shrink: 0;
430
+ background: #333;
431
+ }
432
+
433
+ .suggested-title {
434
+ font-size: 0.85rem;
435
+ font-weight: 500;
436
+ margin: 0;
437
+ display: -webkit-box;
438
+ -webkit-line-clamp: 2;
439
+ -webkit-box-orient: vertical;
440
+ overflow: hidden;
441
+ }
442
+
443
+ .suggested-channel {
444
+ font-size: 0.75rem;
445
+ color: var(--yt-muted);
446
+ margin: 0.25rem 0 0;
447
+ }
448
+
449
+ .loading-tag {
450
+ font-size: 0.7rem;
451
+ color: var(--yt-accent);
452
+ }
453
+
454
+ .embed-badge {
455
+ display: inline-block;
456
+ font-size: 0.65rem;
457
+ color: var(--yt-muted);
458
+ margin-top: 0.2rem;
459
+ }
460
+
461
+ .model-unavailable {
462
+ opacity: 0.55;
463
+ cursor: not-allowed;
464
+ }
465
+
466
+ .model-reason {
467
+ font-size: 0.75rem;
468
+ color: var(--yt-muted);
469
+ font-weight: normal;
470
+ }
471
+
472
+ .error-banner,
473
+ .error-text {
474
+ color: var(--yt-red);
475
+ font-size: 0.85rem;
476
+ }
477
+
478
+ .settings-page,
479
+ .hub-page {
480
+ padding: 1.5rem 2rem 3rem;
481
+ max-width: 960px;
482
+ }
483
+
484
+ .settings-card,
485
+ .chart-box,
486
+ .hub-stat {
487
+ background: var(--yt-surface);
488
+ border-radius: 12px;
489
+ padding: 1.25rem;
490
+ margin-bottom: 1rem;
491
+ }
492
+
493
+ .hint {
494
+ color: var(--yt-muted);
495
+ font-size: 0.85rem;
496
+ }
497
+
498
+ .model-list {
499
+ display: flex;
500
+ flex-direction: column;
501
+ gap: 0.5rem;
502
+ }
503
+
504
+ .model-option {
505
+ display: flex;
506
+ align-items: center;
507
+ gap: 0.5rem;
508
+ cursor: pointer;
509
+ }
510
+
511
+ .settings-msg {
512
+ color: var(--yt-accent);
513
+ font-size: 0.9rem;
514
+ }
515
+
516
+ .hub-cards {
517
+ display: grid;
518
+ grid-template-columns: repeat(3, 1fr);
519
+ gap: 1rem;
520
+ margin-bottom: 1rem;
521
+ }
522
+
523
+ .stat-label {
524
+ display: block;
525
+ color: var(--yt-muted);
526
+ font-size: 0.8rem;
527
+ }
528
+
529
+ .stat-value {
530
+ font-size: 1.5rem;
531
+ font-weight: 700;
532
+ }
533
+
534
+ .hub-charts {
535
+ display: grid;
536
+ grid-template-columns: 1fr 1fr;
537
+ gap: 1rem;
538
+ }
539
+
540
+ @media (max-width: 800px) {
541
+ .hub-charts,
542
+ .hub-cards {
543
+ grid-template-columns: 1fr;
544
+ }
545
+ }
546
+
547
+ .hub-table {
548
+ width: 100%;
549
+ border-collapse: collapse;
550
+ font-size: 0.85rem;
551
+ }
552
+
553
+ .hub-table th,
554
+ .hub-table td {
555
+ text-align: left;
556
+ padding: 0.5rem;
557
+ border-bottom: 1px solid #333;
558
+ }
559
+
560
+ .hub-table-wrap {
561
+ background: var(--yt-surface);
562
+ border-radius: 12px;
563
+ padding: 1rem;
564
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App";
4
+ import "./index.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ );
frontend/src/pages/HubPage.tsx ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Bar,
3
+ BarChart,
4
+ Cell,
5
+ Pie,
6
+ PieChart,
7
+ ResponsiveContainer,
8
+ Tooltip,
9
+ XAxis,
10
+ YAxis,
11
+ } from "recharts";
12
+ import { useApp } from "../context/AppContext";
13
+
14
+ const COLORS = ["#2ba640", "#ff4e45"];
15
+
16
+ export function HubPage() {
17
+ const { hubHistory, threshold } = useApp();
18
+
19
+ const toxic = hubHistory.filter((h) => h.score >= threshold).length;
20
+ const safe = hubHistory.length - toxic;
21
+ const pieData = [
22
+ { name: "Safe", value: safe || 1 },
23
+ { name: "Toxic", value: toxic || 0 },
24
+ ];
25
+
26
+ const barData = hubHistory.slice(0, 12).map((h, i) => ({
27
+ name: `#${i + 1}`,
28
+ score: Math.round(h.score * 100),
29
+ }));
30
+
31
+ return (
32
+ <div className="hub-page">
33
+ <h1>Moderator Hub</h1>
34
+ <div className="hub-cards">
35
+ <div className="hub-stat">
36
+ <span className="stat-label">Threshold</span>
37
+ <span className="stat-value">{threshold.toFixed(2)}</span>
38
+ </div>
39
+ <div className="hub-stat">
40
+ <span className="stat-label">Events logged</span>
41
+ <span className="stat-value">{hubHistory.length}</span>
42
+ </div>
43
+ <div className="hub-stat">
44
+ <span className="stat-label">Toxic (session)</span>
45
+ <span className="stat-value">{toxic}</span>
46
+ </div>
47
+ </div>
48
+
49
+ <div className="hub-charts">
50
+ <div className="chart-box">
51
+ <h2>Safe vs Toxic</h2>
52
+ <ResponsiveContainer width="100%" height={220}>
53
+ <PieChart>
54
+ <Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80}>
55
+ {pieData.map((_, i) => (
56
+ <Cell key={i} fill={COLORS[i % COLORS.length]} />
57
+ ))}
58
+ </Pie>
59
+ <Tooltip />
60
+ </PieChart>
61
+ </ResponsiveContainer>
62
+ </div>
63
+ <div className="chart-box">
64
+ <h2>Recent scores (%)</h2>
65
+ <ResponsiveContainer width="100%" height={220}>
66
+ <BarChart data={barData}>
67
+ <XAxis dataKey="name" stroke="#aaa" />
68
+ <YAxis stroke="#aaa" domain={[0, 100]} />
69
+ <Tooltip />
70
+ <Bar dataKey="score" fill="#3ea6ff" />
71
+ </BarChart>
72
+ </ResponsiveContainer>
73
+ </div>
74
+ </div>
75
+
76
+ <div className="hub-table-wrap">
77
+ <h2>Recent actions</h2>
78
+ <table className="hub-table">
79
+ <thead>
80
+ <tr>
81
+ <th>User</th>
82
+ <th>Comment</th>
83
+ <th>Score</th>
84
+ <th>Action</th>
85
+ </tr>
86
+ </thead>
87
+ <tbody>
88
+ {hubHistory.length === 0 ? (
89
+ <tr>
90
+ <td colSpan={4}>Post comments on the Watch page to populate history.</td>
91
+ </tr>
92
+ ) : (
93
+ hubHistory.map((h, i) => (
94
+ <tr key={i}>
95
+ <td>{h.user}</td>
96
+ <td>{h.snippet}</td>
97
+ <td>{(h.score * 100).toFixed(0)}%</td>
98
+ <td>{h.action}</td>
99
+ </tr>
100
+ ))
101
+ )}
102
+ </tbody>
103
+ </table>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
frontend/src/pages/SettingsPage.tsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { getModelsStatus, predict, setModel } from "../api/client";
3
+ import { useApp } from "../context/AppContext";
4
+ import type { ModelStatusEntry } from "../types/api";
5
+
6
+ export function SettingsPage() {
7
+ const { threshold, setThreshold } = useApp();
8
+ const [modelStatus, setModelStatus] = useState<ModelStatusEntry[]>([]);
9
+ const [active, setActive] = useState("");
10
+ const [testText, setTestText] = useState("You are an idiot");
11
+ const [testResult, setTestResult] = useState<string | null>(null);
12
+ const [testError, setTestError] = useState<string | null>(null);
13
+ const [testing, setTesting] = useState(false);
14
+ const [message, setMessage] = useState<string | null>(null);
15
+
16
+ const loadStatus = () => {
17
+ getModelsStatus()
18
+ .then((r) => {
19
+ setModelStatus(r.models);
20
+ setActive(r.active);
21
+ })
22
+ .catch(() => setMessage("Could not load model status"));
23
+ };
24
+
25
+ useEffect(() => {
26
+ loadStatus();
27
+ }, []);
28
+
29
+ useEffect(() => {
30
+ setTestResult(null);
31
+ setTestError(null);
32
+ }, [testText, threshold]);
33
+
34
+ const switchModel = async (name: string) => {
35
+ const entry = modelStatus.find((m) => m.name === name);
36
+ if (entry && !entry.available) {
37
+ setMessage(entry.reason ?? "Model unavailable");
38
+ return;
39
+ }
40
+ setMessage(null);
41
+ try {
42
+ await setModel(name);
43
+ setActive(name);
44
+ setMessage(`Active model: ${name}`);
45
+ loadStatus();
46
+ } catch (e) {
47
+ setMessage(e instanceof Error ? e.message : "Failed to switch model");
48
+ loadStatus();
49
+ }
50
+ };
51
+
52
+ const runTest = async () => {
53
+ setTesting(true);
54
+ setTestResult(null);
55
+ setTestError(null);
56
+ try {
57
+ const r = await predict(testText, threshold);
58
+ setTestResult(`${r.status} — ${Math.round(r.probability * 100)}% toxic`);
59
+ } catch (e) {
60
+ setTestError(e instanceof Error ? e.message : "Analysis failed");
61
+ } finally {
62
+ setTesting(false);
63
+ }
64
+ };
65
+
66
+ return (
67
+ <div className="settings-page">
68
+ <h1>Settings</h1>
69
+ <section className="settings-card">
70
+ <h2>Active model</h2>
71
+ <p className="hint">
72
+ HF models need <code>uv sync --extra hf</code> locally, or{" "}
73
+ <code>INSTALL_HF=1 docker compose build</code> in Docker.
74
+ </p>
75
+ <div className="model-list">
76
+ {modelStatus.map((m) => (
77
+ <label
78
+ key={m.name}
79
+ className={`model-option ${!m.available ? "model-unavailable" : ""}`}
80
+ >
81
+ <input
82
+ type="radio"
83
+ name="model"
84
+ checked={active === m.name}
85
+ disabled={!m.available}
86
+ onChange={() => void switchModel(m.name)}
87
+ />
88
+ <span>
89
+ {m.name}
90
+ {!m.available && m.reason && (
91
+ <span className="model-reason"> — {m.reason}</span>
92
+ )}
93
+ </span>
94
+ </label>
95
+ ))}
96
+ </div>
97
+ {message && (
98
+ <p className={message.includes("Failed") || message.includes("Install") ? "error-text" : "settings-msg"}>
99
+ {message}
100
+ </p>
101
+ )}
102
+ </section>
103
+
104
+ <section className="settings-card">
105
+ <h2>Toxicity threshold</h2>
106
+ <input
107
+ type="range"
108
+ min={0.1}
109
+ max={0.9}
110
+ step={0.05}
111
+ value={threshold}
112
+ onChange={(e) => setThreshold(Number(e.target.value))}
113
+ />
114
+ <p>{threshold.toFixed(2)} — comments at or above this probability are <strong>Toxic</strong>.</p>
115
+ </section>
116
+
117
+ <section className="settings-card">
118
+ <h2>Quick test</h2>
119
+ <textarea value={testText} onChange={(e) => setTestText(e.target.value)} rows={2} />
120
+ <button
121
+ type="button"
122
+ className="btn-primary"
123
+ disabled={testing || !testText.trim()}
124
+ onClick={() => void runTest()}
125
+ >
126
+ {testing ? "Analyzing…" : "Analyze"}
127
+ </button>
128
+ {testResult && <p className="settings-msg">{testResult}</p>}
129
+ {testError && <p className="error-text">{testError}</p>}
130
+ </section>
131
+ </div>
132
+ );
133
+ }
frontend/src/pages/WatchPage.tsx ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { getSuggestedVideos, predict, predictVideo } from "../api/client";
3
+ import { CommentRow } from "../components/CommentRow";
4
+ import { SuggestedRail } from "../components/SuggestedRail";
5
+ import { useApp } from "../context/AppContext";
6
+ import { useDebouncedPredict } from "../hooks/useDebouncedPredict";
7
+ import type { CommentItem, SuggestedVideo } from "../types/api";
8
+ import { formatPct, newId, toxicityColor } from "../utils/toxicity";
9
+
10
+ function isPlaceholderTitle(title: string, id: string): boolean {
11
+ return title === `Video ${id}`;
12
+ }
13
+
14
+ export function WatchPage() {
15
+ const { threshold, addHubEntry } = useApp();
16
+ const [draft, setDraft] = useState("");
17
+ const [sessionComments, setSessionComments] = useState<CommentItem[]>([]);
18
+ const [suggested, setSuggested] = useState<SuggestedVideo[]>([]);
19
+ const [maxComments, setMaxComments] = useState(50);
20
+ const [activeVideo, setActiveVideo] = useState<SuggestedVideo | null>(null);
21
+ const [youtubeComments, setYoutubeComments] = useState<CommentItem[]>([]);
22
+ const [loadingVideoId, setLoadingVideoId] = useState<string | null>(null);
23
+ const [fetchError, setFetchError] = useState<string | null>(null);
24
+ const [demoBanner, setDemoBanner] = useState(false);
25
+ const [dismissDemoBanner, setDismissDemoBanner] = useState(false);
26
+
27
+ const { result, loading, error } = useDebouncedPredict(draft, threshold);
28
+
29
+ useEffect(() => {
30
+ getSuggestedVideos()
31
+ .then((r) => {
32
+ setSuggested(r.videos);
33
+ setMaxComments(r.max_comments);
34
+ })
35
+ .catch(() => setFetchError("Could not load suggested videos"));
36
+ }, []);
37
+
38
+ const handlePost = useCallback(async () => {
39
+ const text = draft.trim();
40
+ if (!text) return;
41
+ const analysis = result ?? (await predict(text, threshold));
42
+ const item: CommentItem = {
43
+ id: newId(),
44
+ user: "you",
45
+ text,
46
+ time: "just now",
47
+ is_toxic: analysis.is_toxic,
48
+ probability: analysis.probability,
49
+ labels: analysis.labels,
50
+ source: "manual",
51
+ };
52
+ setSessionComments((prev) => [...prev, item]);
53
+ addHubEntry({
54
+ user: "@you",
55
+ snippet: text.slice(0, 45),
56
+ score: analysis.probability,
57
+ action: analysis.is_toxic ? "Posted (toxic)" : "Approved",
58
+ });
59
+ setDraft("");
60
+ }, [draft, result, threshold, addHubEntry]);
61
+
62
+ const loadVideo = async (video: SuggestedVideo) => {
63
+ setActiveVideo(video);
64
+ setYoutubeComments([]);
65
+ setSessionComments([]);
66
+ setFetchError(null);
67
+ setDismissDemoBanner(false);
68
+ setLoadingVideoId(video.id);
69
+ try {
70
+ const res = await predictVideo(video.watch_url, maxComments, threshold);
71
+ setDemoBanner(res.source === "demo");
72
+ setYoutubeComments(
73
+ res.results.map((r, i) => ({
74
+ id: `yt-${video.id}-${i}`,
75
+ user: `viewer_${i + 1}`,
76
+ text: r.text,
77
+ time: "from YouTube",
78
+ is_toxic: r.is_toxic,
79
+ probability: r.probability,
80
+ labels: r.labels,
81
+ source: "youtube" as const,
82
+ }))
83
+ );
84
+ } catch (e) {
85
+ setFetchError(e instanceof Error ? e.message : "Failed to load comments");
86
+ setYoutubeComments([]);
87
+ setDemoBanner(false);
88
+ } finally {
89
+ setLoadingVideoId(null);
90
+ }
91
+ };
92
+
93
+ const toxicManual = sessionComments.filter((c) => c.is_toxic).length;
94
+ const toxicYt = youtubeComments.filter((c) => c.is_toxic).length;
95
+ const totalComments = sessionComments.length + youtubeComments.length;
96
+
97
+ const channelInitial = activeVideo?.channel_title?.charAt(0).toUpperCase() ?? "Y";
98
+
99
+ return (
100
+ <div className="watch-page">
101
+ <div className="watch-grid">
102
+ <section className="primary-column">
103
+ <div className="staged-player">
104
+ {activeVideo && !activeVideo.embeddable ? (
105
+ <a
106
+ className="player-fallback"
107
+ href={activeVideo.watch_url}
108
+ target="_blank"
109
+ rel="noopener noreferrer"
110
+ >
111
+ <img
112
+ src={activeVideo.thumbnail_url}
113
+ alt=""
114
+ className="player-fallback-thumb"
115
+ />
116
+ <span className="player-fallback-cta">Watch on YouTube (embedding blocked)</span>
117
+ </a>
118
+ ) : activeVideo ? (
119
+ <iframe
120
+ className="player-iframe"
121
+ src={`https://www.youtube.com/embed/${activeVideo.id}?autoplay=1&rel=0`}
122
+ title={activeVideo.title}
123
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
124
+ allowFullScreen
125
+ loading="lazy"
126
+ />
127
+ ) : (
128
+ <div className="player-poster">
129
+ <span className="play-icon" aria-hidden>
130
+
131
+ </span>
132
+ <p className="player-hint">Select a video from Up next</p>
133
+ </div>
134
+ )}
135
+ </div>
136
+
137
+ <h1 className="video-title">
138
+ {activeVideo?.title ?? "Select a video from Up next"}
139
+ </h1>
140
+ <p className="video-meta">
141
+ {activeVideo ? activeVideo.channel_title : "Pick a suggested video to start"}
142
+ </p>
143
+
144
+ {activeVideo && isPlaceholderTitle(activeVideo.title, activeVideo.id) && (
145
+ <p className="info-banner">
146
+ Demo metadata — add <code>YOUTUBE_API_KEY</code> to <code>.env</code> for real titles.
147
+ </p>
148
+ )}
149
+
150
+ <div className="channel-row">
151
+ <div className="channel-avatar">{channelInitial}</div>
152
+ <div>
153
+ <p className="channel-name">{activeVideo?.channel_title ?? "YouTube"}</p>
154
+ {activeVideo && <p className="video-meta">Suggested video</p>}
155
+ </div>
156
+ </div>
157
+
158
+ <div className="comments-header">
159
+ <span>
160
+ {totalComments} comments
161
+ {toxicManual + toxicYt > 0 && (
162
+ <span className="toxic-count"> · {toxicManual + toxicYt} toxic detected</span>
163
+ )}
164
+ </span>
165
+ </div>
166
+
167
+ <div className="comment-compose">
168
+ <textarea
169
+ value={draft}
170
+ onChange={(e) => setDraft(e.target.value)}
171
+ onKeyDown={(e) => {
172
+ if (e.key === "Enter" && !e.shiftKey) {
173
+ e.preventDefault();
174
+ void handlePost();
175
+ }
176
+ }}
177
+ placeholder="Add a comment…"
178
+ rows={3}
179
+ aria-label="Write a comment"
180
+ />
181
+ {draft.trim() && (
182
+ <div className="live-analysis">
183
+ <span>{loading ? "Analyzing…" : "Live score"}</span>
184
+ {result && (
185
+ <>
186
+ <span className={`badge ${result.is_toxic ? "badge-toxic" : "badge-safe"}`}>
187
+ {result.status}
188
+ </span>
189
+ <span style={{ color: toxicityColor(result.probability) }}>
190
+ Toxicity: {formatPct(result.probability)}
191
+ </span>
192
+ </>
193
+ )}
194
+ {error && <span className="error-text">{error}</span>}
195
+ </div>
196
+ )}
197
+ <div className="compose-actions">
198
+ <button type="button" className="btn-secondary" onClick={() => setDraft("")}>
199
+ Cancel
200
+ </button>
201
+ <button type="button" className="btn-primary" onClick={() => void handlePost()}>
202
+ Comment
203
+ </button>
204
+ </div>
205
+ </div>
206
+
207
+ {fetchError && <p className="error-banner">{fetchError}</p>}
208
+
209
+ {demoBanner && !dismissDemoBanner && (
210
+ <div className="info-banner dismissible">
211
+ <span>
212
+ Using demo comments — add <code>YOUTUBE_API_KEY</code> to <code>.env</code> for real
213
+ YouTube threads.
214
+ </span>
215
+ <button
216
+ type="button"
217
+ className="btn-dismiss"
218
+ onClick={() => setDismissDemoBanner(true)}
219
+ aria-label="Dismiss"
220
+ >
221
+ ×
222
+ </button>
223
+ </div>
224
+ )}
225
+
226
+ {loadingVideoId && youtubeComments.length === 0 && (
227
+ <p className="loading-comments">Loading comments…</p>
228
+ )}
229
+
230
+ <div className="comment-list">
231
+ {youtubeComments.map((c) => (
232
+ <CommentRow key={c.id} comment={c} />
233
+ ))}
234
+ {[...sessionComments].reverse().map((c) => (
235
+ <CommentRow key={c.id} comment={c} />
236
+ ))}
237
+ </div>
238
+ </section>
239
+
240
+ <SuggestedRail
241
+ videos={suggested}
242
+ activeId={activeVideo?.id ?? null}
243
+ loadingId={loadingVideoId}
244
+ onSelect={(v) => void loadVideo(v)}
245
+ />
246
+ </div>
247
+ </div>
248
+ );
249
+ }
frontend/src/types/api.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type PredictResponse = {
2
+ text: string;
3
+ is_toxic: boolean;
4
+ probability: number;
5
+ status: "Safe" | "Toxic";
6
+ mode: "binary";
7
+ labels: string[];
8
+ model_used: string;
9
+ latency_ms: number;
10
+ };
11
+
12
+ export type SuggestedVideo = {
13
+ id: string;
14
+ title: string;
15
+ channel_title: string;
16
+ thumbnail_url: string;
17
+ watch_url: string;
18
+ embeddable: boolean;
19
+ };
20
+
21
+ export type VideoResponse = {
22
+ video_url: string;
23
+ total_fetched: number;
24
+ toxic_count: number;
25
+ toxic_rate: number;
26
+ results: PredictResponse[];
27
+ source: "youtube" | "demo" | "unavailable";
28
+ reason?: string;
29
+ };
30
+
31
+ export type ModelStatusEntry = {
32
+ name: string;
33
+ available: boolean;
34
+ reason: string | null;
35
+ type: string;
36
+ };
37
+
38
+ export type CommentItem = {
39
+ id: string;
40
+ user: string;
41
+ text: string;
42
+ time: string;
43
+ is_toxic: boolean;
44
+ probability: number;
45
+ labels: string[];
46
+ source: "manual" | "youtube";
47
+ };
frontend/src/utils/toxicity.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function toxicityColor(probability: number): string {
2
+ const pct = probability * 100;
3
+ if (pct >= 70) return "#ff4e45";
4
+ if (pct >= 40) return "#f5a623";
5
+ return "#2ba640";
6
+ }
7
+
8
+ export function formatPct(probability: number): string {
9
+ return `${Math.round(probability * 100)}%`;
10
+ }
11
+
12
+ export function newId(): string {
13
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
14
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true
19
+ },
20
+ "include": ["src"]
21
+ }
frontend/tsconfig.node.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+ "moduleResolution": "bundler"
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ "/predict": "http://127.0.0.1:8000",
10
+ "/predict-batch": "http://127.0.0.1:8000",
11
+ "/predict-video": "http://127.0.0.1:8000",
12
+ "/models": "http://127.0.0.1:8000",
13
+ "/model-info": "http://127.0.0.1:8000",
14
+ "/model": "http://127.0.0.1:8000",
15
+ "/videos": "http://127.0.0.1:8000",
16
+ },
17
+ },
18
+ });