Jiya3177 commited on
Commit
3a376b8
·
1 Parent(s): aaff8ef

refactor: migrate frontend state to zustand

Browse files
frontend/package-lock.json CHANGED
@@ -22,7 +22,8 @@
22
  "remark-gfm": "^4.0.1",
23
  "shadcn": "^4.3.1",
24
  "tailwind-merge": "^3.5.0",
25
- "tw-animate-css": "^1.4.0"
 
26
  },
27
  "devDependencies": {
28
  "@tailwindcss/postcss": "^4",
@@ -76,6 +77,7 @@
76
  "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
77
  "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
78
  "license": "MIT",
 
79
  "dependencies": {
80
  "@babel/code-frame": "^7.29.0",
81
  "@babel/generator": "^7.29.0",
@@ -669,6 +671,7 @@
669
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
670
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
671
  "license": "MIT",
 
672
  "engines": {
673
  "node": ">=12"
674
  },
@@ -1106,9 +1109,6 @@
1106
  "cpu": [
1107
  "arm"
1108
  ],
1109
- "libc": [
1110
- "glibc"
1111
- ],
1112
  "license": "LGPL-3.0-or-later",
1113
  "optional": true,
1114
  "os": [
@@ -1125,9 +1125,6 @@
1125
  "cpu": [
1126
  "arm64"
1127
  ],
1128
- "libc": [
1129
- "glibc"
1130
- ],
1131
  "license": "LGPL-3.0-or-later",
1132
  "optional": true,
1133
  "os": [
@@ -1144,9 +1141,6 @@
1144
  "cpu": [
1145
  "ppc64"
1146
  ],
1147
- "libc": [
1148
- "glibc"
1149
- ],
1150
  "license": "LGPL-3.0-or-later",
1151
  "optional": true,
1152
  "os": [
@@ -1163,9 +1157,6 @@
1163
  "cpu": [
1164
  "riscv64"
1165
  ],
1166
- "libc": [
1167
- "glibc"
1168
- ],
1169
  "license": "LGPL-3.0-or-later",
1170
  "optional": true,
1171
  "os": [
@@ -1182,9 +1173,6 @@
1182
  "cpu": [
1183
  "s390x"
1184
  ],
1185
- "libc": [
1186
- "glibc"
1187
- ],
1188
  "license": "LGPL-3.0-or-later",
1189
  "optional": true,
1190
  "os": [
@@ -1201,9 +1189,6 @@
1201
  "cpu": [
1202
  "x64"
1203
  ],
1204
- "libc": [
1205
- "glibc"
1206
- ],
1207
  "license": "LGPL-3.0-or-later",
1208
  "optional": true,
1209
  "os": [
@@ -1220,9 +1205,6 @@
1220
  "cpu": [
1221
  "arm64"
1222
  ],
1223
- "libc": [
1224
- "musl"
1225
- ],
1226
  "license": "LGPL-3.0-or-later",
1227
  "optional": true,
1228
  "os": [
@@ -1239,9 +1221,6 @@
1239
  "cpu": [
1240
  "x64"
1241
  ],
1242
- "libc": [
1243
- "musl"
1244
- ],
1245
  "license": "LGPL-3.0-or-later",
1246
  "optional": true,
1247
  "os": [
@@ -1258,9 +1237,6 @@
1258
  "cpu": [
1259
  "arm"
1260
  ],
1261
- "libc": [
1262
- "glibc"
1263
- ],
1264
  "license": "Apache-2.0",
1265
  "optional": true,
1266
  "os": [
@@ -1283,9 +1259,6 @@
1283
  "cpu": [
1284
  "arm64"
1285
  ],
1286
- "libc": [
1287
- "glibc"
1288
- ],
1289
  "license": "Apache-2.0",
1290
  "optional": true,
1291
  "os": [
@@ -1308,9 +1281,6 @@
1308
  "cpu": [
1309
  "ppc64"
1310
  ],
1311
- "libc": [
1312
- "glibc"
1313
- ],
1314
  "license": "Apache-2.0",
1315
  "optional": true,
1316
  "os": [
@@ -1333,9 +1303,6 @@
1333
  "cpu": [
1334
  "riscv64"
1335
  ],
1336
- "libc": [
1337
- "glibc"
1338
- ],
1339
  "license": "Apache-2.0",
1340
  "optional": true,
1341
  "os": [
@@ -1358,9 +1325,6 @@
1358
  "cpu": [
1359
  "s390x"
1360
  ],
1361
- "libc": [
1362
- "glibc"
1363
- ],
1364
  "license": "Apache-2.0",
1365
  "optional": true,
1366
  "os": [
@@ -1383,9 +1347,6 @@
1383
  "cpu": [
1384
  "x64"
1385
  ],
1386
- "libc": [
1387
- "glibc"
1388
- ],
1389
  "license": "Apache-2.0",
1390
  "optional": true,
1391
  "os": [
@@ -1408,9 +1369,6 @@
1408
  "cpu": [
1409
  "arm64"
1410
  ],
1411
- "libc": [
1412
- "musl"
1413
- ],
1414
  "license": "Apache-2.0",
1415
  "optional": true,
1416
  "os": [
@@ -1433,9 +1391,6 @@
1433
  "cpu": [
1434
  "x64"
1435
  ],
1436
- "libc": [
1437
- "musl"
1438
- ],
1439
  "license": "Apache-2.0",
1440
  "optional": true,
1441
  "os": [
@@ -1856,9 +1811,6 @@
1856
  "cpu": [
1857
  "arm64"
1858
  ],
1859
- "libc": [
1860
- "glibc"
1861
- ],
1862
  "license": "MIT",
1863
  "optional": true,
1864
  "os": [
@@ -1879,9 +1831,6 @@
1879
  "cpu": [
1880
  "arm64"
1881
  ],
1882
- "libc": [
1883
- "musl"
1884
- ],
1885
  "license": "MIT",
1886
  "optional": true,
1887
  "os": [
@@ -1902,9 +1851,6 @@
1902
  "cpu": [
1903
  "riscv64"
1904
  ],
1905
- "libc": [
1906
- "glibc"
1907
- ],
1908
  "license": "MIT",
1909
  "optional": true,
1910
  "os": [
@@ -1925,9 +1871,6 @@
1925
  "cpu": [
1926
  "x64"
1927
  ],
1928
- "libc": [
1929
- "glibc"
1930
- ],
1931
  "license": "MIT",
1932
  "optional": true,
1933
  "os": [
@@ -1948,9 +1891,6 @@
1948
  "cpu": [
1949
  "x64"
1950
  ],
1951
- "libc": [
1952
- "musl"
1953
- ],
1954
  "license": "MIT",
1955
  "optional": true,
1956
  "os": [
@@ -2072,9 +2012,6 @@
2072
  "cpu": [
2073
  "arm64"
2074
  ],
2075
- "libc": [
2076
- "glibc"
2077
- ],
2078
  "license": "MIT",
2079
  "optional": true,
2080
  "os": [
@@ -2091,9 +2028,6 @@
2091
  "cpu": [
2092
  "arm64"
2093
  ],
2094
- "libc": [
2095
- "musl"
2096
- ],
2097
  "license": "MIT",
2098
  "optional": true,
2099
  "os": [
@@ -2110,9 +2044,6 @@
2110
  "cpu": [
2111
  "x64"
2112
  ],
2113
- "libc": [
2114
- "glibc"
2115
- ],
2116
  "license": "MIT",
2117
  "optional": true,
2118
  "os": [
@@ -2129,9 +2060,6 @@
2129
  "cpu": [
2130
  "x64"
2131
  ],
2132
- "libc": [
2133
- "musl"
2134
- ],
2135
  "license": "MIT",
2136
  "optional": true,
2137
  "os": [
@@ -2178,6 +2106,7 @@
2178
  "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
2179
  "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
2180
  "license": "MIT",
 
2181
  "engines": {
2182
  "node": "^14.21.3 || >=16"
2183
  },
@@ -2446,9 +2375,6 @@
2446
  "arm64"
2447
  ],
2448
  "dev": true,
2449
- "libc": [
2450
- "glibc"
2451
- ],
2452
  "license": "MIT",
2453
  "optional": true,
2454
  "os": [
@@ -2466,9 +2392,6 @@
2466
  "arm64"
2467
  ],
2468
  "dev": true,
2469
- "libc": [
2470
- "musl"
2471
- ],
2472
  "license": "MIT",
2473
  "optional": true,
2474
  "os": [
@@ -2486,9 +2409,6 @@
2486
  "x64"
2487
  ],
2488
  "dev": true,
2489
- "libc": [
2490
- "glibc"
2491
- ],
2492
  "license": "MIT",
2493
  "optional": true,
2494
  "os": [
@@ -2506,9 +2426,6 @@
2506
  "x64"
2507
  ],
2508
  "dev": true,
2509
- "libc": [
2510
- "musl"
2511
- ],
2512
  "license": "MIT",
2513
  "optional": true,
2514
  "os": [
@@ -2749,6 +2666,7 @@
2749
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
2750
  "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
2751
  "license": "MIT",
 
2752
  "dependencies": {
2753
  "undici-types": "~6.21.0"
2754
  }
@@ -2758,6 +2676,7 @@
2758
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
2759
  "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
2760
  "license": "MIT",
 
2761
  "dependencies": {
2762
  "csstype": "^3.2.2"
2763
  }
@@ -2844,6 +2763,7 @@
2844
  "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
2845
  "dev": true,
2846
  "license": "MIT",
 
2847
  "dependencies": {
2848
  "@typescript-eslint/scope-manager": "8.59.0",
2849
  "@typescript-eslint/types": "8.59.0",
@@ -3206,9 +3126,6 @@
3206
  "arm64"
3207
  ],
3208
  "dev": true,
3209
- "libc": [
3210
- "glibc"
3211
- ],
3212
  "license": "MIT",
3213
  "optional": true,
3214
  "os": [
@@ -3223,9 +3140,6 @@
3223
  "arm64"
3224
  ],
3225
  "dev": true,
3226
- "libc": [
3227
- "musl"
3228
- ],
3229
  "license": "MIT",
3230
  "optional": true,
3231
  "os": [
@@ -3240,9 +3154,6 @@
3240
  "ppc64"
3241
  ],
3242
  "dev": true,
3243
- "libc": [
3244
- "glibc"
3245
- ],
3246
  "license": "MIT",
3247
  "optional": true,
3248
  "os": [
@@ -3257,9 +3168,6 @@
3257
  "riscv64"
3258
  ],
3259
  "dev": true,
3260
- "libc": [
3261
- "glibc"
3262
- ],
3263
  "license": "MIT",
3264
  "optional": true,
3265
  "os": [
@@ -3274,9 +3182,6 @@
3274
  "riscv64"
3275
  ],
3276
  "dev": true,
3277
- "libc": [
3278
- "musl"
3279
- ],
3280
  "license": "MIT",
3281
  "optional": true,
3282
  "os": [
@@ -3291,9 +3196,6 @@
3291
  "s390x"
3292
  ],
3293
  "dev": true,
3294
- "libc": [
3295
- "glibc"
3296
- ],
3297
  "license": "MIT",
3298
  "optional": true,
3299
  "os": [
@@ -3308,9 +3210,6 @@
3308
  "x64"
3309
  ],
3310
  "dev": true,
3311
- "libc": [
3312
- "glibc"
3313
- ],
3314
  "license": "MIT",
3315
  "optional": true,
3316
  "os": [
@@ -3325,9 +3224,6 @@
3325
  "x64"
3326
  ],
3327
  "dev": true,
3328
- "libc": [
3329
- "musl"
3330
- ],
3331
  "license": "MIT",
3332
  "optional": true,
3333
  "os": [
@@ -3412,6 +3308,7 @@
3412
  "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
3413
  "dev": true,
3414
  "license": "MIT",
 
3415
  "bin": {
3416
  "acorn": "bin/acorn"
3417
  },
@@ -3866,6 +3763,7 @@
3866
  }
3867
  ],
3868
  "license": "MIT",
 
3869
  "dependencies": {
3870
  "baseline-browser-mapping": "^2.10.12",
3871
  "caniuse-lite": "^1.0.30001782",
@@ -4917,6 +4815,7 @@
4917
  "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
4918
  "dev": true,
4919
  "license": "MIT",
 
4920
  "dependencies": {
4921
  "@eslint-community/eslint-utils": "^4.8.0",
4922
  "@eslint-community/regexpp": "^4.12.1",
@@ -5102,6 +5001,7 @@
5102
  "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
5103
  "dev": true,
5104
  "license": "MIT",
 
5105
  "dependencies": {
5106
  "@rtsao/scc": "^1.1.0",
5107
  "array-includes": "^3.1.9",
@@ -5401,6 +5301,7 @@
5401
  "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
5402
  "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
5403
  "license": "MIT",
 
5404
  "dependencies": {
5405
  "accepts": "^2.0.0",
5406
  "body-parser": "^2.2.1",
@@ -6172,6 +6073,7 @@
6172
  "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
6173
  "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
6174
  "license": "MIT",
 
6175
  "engines": {
6176
  "node": ">=16.9.0"
6177
  }
@@ -7288,9 +7190,6 @@
7288
  "arm64"
7289
  ],
7290
  "dev": true,
7291
- "libc": [
7292
- "glibc"
7293
- ],
7294
  "license": "MPL-2.0",
7295
  "optional": true,
7296
  "os": [
@@ -7312,9 +7211,6 @@
7312
  "arm64"
7313
  ],
7314
  "dev": true,
7315
- "libc": [
7316
- "musl"
7317
- ],
7318
  "license": "MPL-2.0",
7319
  "optional": true,
7320
  "os": [
@@ -7336,9 +7232,6 @@
7336
  "x64"
7337
  ],
7338
  "dev": true,
7339
- "libc": [
7340
- "glibc"
7341
- ],
7342
  "license": "MPL-2.0",
7343
  "optional": true,
7344
  "os": [
@@ -7360,9 +7253,6 @@
7360
  "x64"
7361
  ],
7362
  "dev": true,
7363
- "libc": [
7364
- "musl"
7365
- ],
7366
  "license": "MPL-2.0",
7367
  "optional": true,
7368
  "os": [
@@ -9521,6 +9411,7 @@
9521
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
9522
  "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
9523
  "license": "MIT",
 
9524
  "engines": {
9525
  "node": ">=0.10.0"
9526
  }
@@ -9530,6 +9421,7 @@
9530
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
9531
  "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
9532
  "license": "MIT",
 
9533
  "dependencies": {
9534
  "scheduler": "^0.27.0"
9535
  },
@@ -10810,6 +10702,7 @@
10810
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
10811
  "dev": true,
10812
  "license": "MIT",
 
10813
  "engines": {
10814
  "node": ">=12"
10815
  },
@@ -11078,6 +10971,7 @@
11078
  "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
11079
  "devOptional": true,
11080
  "license": "Apache-2.0",
 
11081
  "bin": {
11082
  "tsc": "bin/tsc",
11083
  "tsserver": "bin/tsserver"
@@ -11737,6 +11631,7 @@
11737
  "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
11738
  "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
11739
  "license": "MIT",
 
11740
  "funding": {
11741
  "url": "https://github.com/sponsors/colinhacks"
11742
  }
@@ -11763,6 +11658,35 @@
11763
  "zod": "^3.25.0 || ^4.0.0"
11764
  }
11765
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11766
  "node_modules/zwitch": {
11767
  "version": "2.0.4",
11768
  "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
 
22
  "remark-gfm": "^4.0.1",
23
  "shadcn": "^4.3.1",
24
  "tailwind-merge": "^3.5.0",
25
+ "tw-animate-css": "^1.4.0",
26
+ "zustand": "^5.0.13"
27
  },
28
  "devDependencies": {
29
  "@tailwindcss/postcss": "^4",
 
77
  "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
78
  "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
79
  "license": "MIT",
80
+ "peer": true,
81
  "dependencies": {
82
  "@babel/code-frame": "^7.29.0",
83
  "@babel/generator": "^7.29.0",
 
671
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
672
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
673
  "license": "MIT",
674
+ "peer": true,
675
  "engines": {
676
  "node": ">=12"
677
  },
 
1109
  "cpu": [
1110
  "arm"
1111
  ],
 
 
 
1112
  "license": "LGPL-3.0-or-later",
1113
  "optional": true,
1114
  "os": [
 
1125
  "cpu": [
1126
  "arm64"
1127
  ],
 
 
 
1128
  "license": "LGPL-3.0-or-later",
1129
  "optional": true,
1130
  "os": [
 
1141
  "cpu": [
1142
  "ppc64"
1143
  ],
 
 
 
1144
  "license": "LGPL-3.0-or-later",
1145
  "optional": true,
1146
  "os": [
 
1157
  "cpu": [
1158
  "riscv64"
1159
  ],
 
 
 
1160
  "license": "LGPL-3.0-or-later",
1161
  "optional": true,
1162
  "os": [
 
1173
  "cpu": [
1174
  "s390x"
1175
  ],
 
 
 
1176
  "license": "LGPL-3.0-or-later",
1177
  "optional": true,
1178
  "os": [
 
1189
  "cpu": [
1190
  "x64"
1191
  ],
 
 
 
1192
  "license": "LGPL-3.0-or-later",
1193
  "optional": true,
1194
  "os": [
 
1205
  "cpu": [
1206
  "arm64"
1207
  ],
 
 
 
1208
  "license": "LGPL-3.0-or-later",
1209
  "optional": true,
1210
  "os": [
 
1221
  "cpu": [
1222
  "x64"
1223
  ],
 
 
 
1224
  "license": "LGPL-3.0-or-later",
1225
  "optional": true,
1226
  "os": [
 
1237
  "cpu": [
1238
  "arm"
1239
  ],
 
 
 
1240
  "license": "Apache-2.0",
1241
  "optional": true,
1242
  "os": [
 
1259
  "cpu": [
1260
  "arm64"
1261
  ],
 
 
 
1262
  "license": "Apache-2.0",
1263
  "optional": true,
1264
  "os": [
 
1281
  "cpu": [
1282
  "ppc64"
1283
  ],
 
 
 
1284
  "license": "Apache-2.0",
1285
  "optional": true,
1286
  "os": [
 
1303
  "cpu": [
1304
  "riscv64"
1305
  ],
 
 
 
1306
  "license": "Apache-2.0",
1307
  "optional": true,
1308
  "os": [
 
1325
  "cpu": [
1326
  "s390x"
1327
  ],
 
 
 
1328
  "license": "Apache-2.0",
1329
  "optional": true,
1330
  "os": [
 
1347
  "cpu": [
1348
  "x64"
1349
  ],
 
 
 
1350
  "license": "Apache-2.0",
1351
  "optional": true,
1352
  "os": [
 
1369
  "cpu": [
1370
  "arm64"
1371
  ],
 
 
 
1372
  "license": "Apache-2.0",
1373
  "optional": true,
1374
  "os": [
 
1391
  "cpu": [
1392
  "x64"
1393
  ],
 
 
 
1394
  "license": "Apache-2.0",
1395
  "optional": true,
1396
  "os": [
 
1811
  "cpu": [
1812
  "arm64"
1813
  ],
 
 
 
1814
  "license": "MIT",
1815
  "optional": true,
1816
  "os": [
 
1831
  "cpu": [
1832
  "arm64"
1833
  ],
 
 
 
1834
  "license": "MIT",
1835
  "optional": true,
1836
  "os": [
 
1851
  "cpu": [
1852
  "riscv64"
1853
  ],
 
 
 
1854
  "license": "MIT",
1855
  "optional": true,
1856
  "os": [
 
1871
  "cpu": [
1872
  "x64"
1873
  ],
 
 
 
1874
  "license": "MIT",
1875
  "optional": true,
1876
  "os": [
 
1891
  "cpu": [
1892
  "x64"
1893
  ],
 
 
 
1894
  "license": "MIT",
1895
  "optional": true,
1896
  "os": [
 
2012
  "cpu": [
2013
  "arm64"
2014
  ],
 
 
 
2015
  "license": "MIT",
2016
  "optional": true,
2017
  "os": [
 
2028
  "cpu": [
2029
  "arm64"
2030
  ],
 
 
 
2031
  "license": "MIT",
2032
  "optional": true,
2033
  "os": [
 
2044
  "cpu": [
2045
  "x64"
2046
  ],
 
 
 
2047
  "license": "MIT",
2048
  "optional": true,
2049
  "os": [
 
2060
  "cpu": [
2061
  "x64"
2062
  ],
 
 
 
2063
  "license": "MIT",
2064
  "optional": true,
2065
  "os": [
 
2106
  "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
2107
  "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
2108
  "license": "MIT",
2109
+ "peer": true,
2110
  "engines": {
2111
  "node": "^14.21.3 || >=16"
2112
  },
 
2375
  "arm64"
2376
  ],
2377
  "dev": true,
 
 
 
2378
  "license": "MIT",
2379
  "optional": true,
2380
  "os": [
 
2392
  "arm64"
2393
  ],
2394
  "dev": true,
 
 
 
2395
  "license": "MIT",
2396
  "optional": true,
2397
  "os": [
 
2409
  "x64"
2410
  ],
2411
  "dev": true,
 
 
 
2412
  "license": "MIT",
2413
  "optional": true,
2414
  "os": [
 
2426
  "x64"
2427
  ],
2428
  "dev": true,
 
 
 
2429
  "license": "MIT",
2430
  "optional": true,
2431
  "os": [
 
2666
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
2667
  "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
2668
  "license": "MIT",
2669
+ "peer": true,
2670
  "dependencies": {
2671
  "undici-types": "~6.21.0"
2672
  }
 
2676
  "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
2677
  "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
2678
  "license": "MIT",
2679
+ "peer": true,
2680
  "dependencies": {
2681
  "csstype": "^3.2.2"
2682
  }
 
2763
  "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
2764
  "dev": true,
2765
  "license": "MIT",
2766
+ "peer": true,
2767
  "dependencies": {
2768
  "@typescript-eslint/scope-manager": "8.59.0",
2769
  "@typescript-eslint/types": "8.59.0",
 
3126
  "arm64"
3127
  ],
3128
  "dev": true,
 
 
 
3129
  "license": "MIT",
3130
  "optional": true,
3131
  "os": [
 
3140
  "arm64"
3141
  ],
3142
  "dev": true,
 
 
 
3143
  "license": "MIT",
3144
  "optional": true,
3145
  "os": [
 
3154
  "ppc64"
3155
  ],
3156
  "dev": true,
 
 
 
3157
  "license": "MIT",
3158
  "optional": true,
3159
  "os": [
 
3168
  "riscv64"
3169
  ],
3170
  "dev": true,
 
 
 
3171
  "license": "MIT",
3172
  "optional": true,
3173
  "os": [
 
3182
  "riscv64"
3183
  ],
3184
  "dev": true,
 
 
 
3185
  "license": "MIT",
3186
  "optional": true,
3187
  "os": [
 
3196
  "s390x"
3197
  ],
3198
  "dev": true,
 
 
 
3199
  "license": "MIT",
3200
  "optional": true,
3201
  "os": [
 
3210
  "x64"
3211
  ],
3212
  "dev": true,
 
 
 
3213
  "license": "MIT",
3214
  "optional": true,
3215
  "os": [
 
3224
  "x64"
3225
  ],
3226
  "dev": true,
 
 
 
3227
  "license": "MIT",
3228
  "optional": true,
3229
  "os": [
 
3308
  "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
3309
  "dev": true,
3310
  "license": "MIT",
3311
+ "peer": true,
3312
  "bin": {
3313
  "acorn": "bin/acorn"
3314
  },
 
3763
  }
3764
  ],
3765
  "license": "MIT",
3766
+ "peer": true,
3767
  "dependencies": {
3768
  "baseline-browser-mapping": "^2.10.12",
3769
  "caniuse-lite": "^1.0.30001782",
 
4815
  "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
4816
  "dev": true,
4817
  "license": "MIT",
4818
+ "peer": true,
4819
  "dependencies": {
4820
  "@eslint-community/eslint-utils": "^4.8.0",
4821
  "@eslint-community/regexpp": "^4.12.1",
 
5001
  "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
5002
  "dev": true,
5003
  "license": "MIT",
5004
+ "peer": true,
5005
  "dependencies": {
5006
  "@rtsao/scc": "^1.1.0",
5007
  "array-includes": "^3.1.9",
 
5301
  "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
5302
  "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
5303
  "license": "MIT",
5304
+ "peer": true,
5305
  "dependencies": {
5306
  "accepts": "^2.0.0",
5307
  "body-parser": "^2.2.1",
 
6073
  "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
6074
  "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
6075
  "license": "MIT",
6076
+ "peer": true,
6077
  "engines": {
6078
  "node": ">=16.9.0"
6079
  }
 
7190
  "arm64"
7191
  ],
7192
  "dev": true,
 
 
 
7193
  "license": "MPL-2.0",
7194
  "optional": true,
7195
  "os": [
 
7211
  "arm64"
7212
  ],
7213
  "dev": true,
 
 
 
7214
  "license": "MPL-2.0",
7215
  "optional": true,
7216
  "os": [
 
7232
  "x64"
7233
  ],
7234
  "dev": true,
 
 
 
7235
  "license": "MPL-2.0",
7236
  "optional": true,
7237
  "os": [
 
7253
  "x64"
7254
  ],
7255
  "dev": true,
 
 
 
7256
  "license": "MPL-2.0",
7257
  "optional": true,
7258
  "os": [
 
9411
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
9412
  "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
9413
  "license": "MIT",
9414
+ "peer": true,
9415
  "engines": {
9416
  "node": ">=0.10.0"
9417
  }
 
9421
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
9422
  "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
9423
  "license": "MIT",
9424
+ "peer": true,
9425
  "dependencies": {
9426
  "scheduler": "^0.27.0"
9427
  },
 
10702
  "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
10703
  "dev": true,
10704
  "license": "MIT",
10705
+ "peer": true,
10706
  "engines": {
10707
  "node": ">=12"
10708
  },
 
10971
  "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
10972
  "devOptional": true,
10973
  "license": "Apache-2.0",
10974
+ "peer": true,
10975
  "bin": {
10976
  "tsc": "bin/tsc",
10977
  "tsserver": "bin/tsserver"
 
11631
  "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
11632
  "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
11633
  "license": "MIT",
11634
+ "peer": true,
11635
  "funding": {
11636
  "url": "https://github.com/sponsors/colinhacks"
11637
  }
 
11658
  "zod": "^3.25.0 || ^4.0.0"
11659
  }
11660
  },
11661
+ "node_modules/zustand": {
11662
+ "version": "5.0.13",
11663
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz",
11664
+ "integrity": "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ==",
11665
+ "license": "MIT",
11666
+ "engines": {
11667
+ "node": ">=12.20.0"
11668
+ },
11669
+ "peerDependencies": {
11670
+ "@types/react": ">=18.0.0",
11671
+ "immer": ">=9.0.6",
11672
+ "react": ">=18.0.0",
11673
+ "use-sync-external-store": ">=1.2.0"
11674
+ },
11675
+ "peerDependenciesMeta": {
11676
+ "@types/react": {
11677
+ "optional": true
11678
+ },
11679
+ "immer": {
11680
+ "optional": true
11681
+ },
11682
+ "react": {
11683
+ "optional": true
11684
+ },
11685
+ "use-sync-external-store": {
11686
+ "optional": true
11687
+ }
11688
+ }
11689
+ },
11690
  "node_modules/zwitch": {
11691
  "version": "2.0.4",
11692
  "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
frontend/package.json CHANGED
@@ -23,7 +23,8 @@
23
  "remark-gfm": "^4.0.1",
24
  "shadcn": "^4.3.1",
25
  "tailwind-merge": "^3.5.0",
26
- "tw-animate-css": "^1.4.0"
 
27
  },
28
  "devDependencies": {
29
  "@tailwindcss/postcss": "^4",
 
23
  "remark-gfm": "^4.0.1",
24
  "shadcn": "^4.3.1",
25
  "tailwind-merge": "^3.5.0",
26
+ "tw-animate-css": "^1.4.0",
27
+ "zustand": "^5.0.13"
28
  },
29
  "devDependencies": {
30
  "@tailwindcss/postcss": "^4",
frontend/src/components/chat/ChatPanel.tsx CHANGED
@@ -3,38 +3,28 @@
3
  import { useState, useRef, useEffect } from "react";
4
  import type { DocInfo } from "@/app/dashboard/page";
5
  import { api, API_BASE } from "@/lib/api";
 
6
  import { Button } from "@/components/ui/button";
7
  import { Textarea } from "@/components/ui/textarea";
8
  import MessageBubble from "./MessageBubble";
9
  import SourceCard from "./SourceCard";
10
  import { Send, Loader2, Trash2, MessageSquare, Download } from "lucide-react";
11
 
12
- export interface SourceChunk {
13
- text: string;
14
- filename: string;
15
- page: number;
16
- score: number;
17
- confidence: number;
18
- }
19
-
20
- export interface ChatMsg {
21
- id: string;
22
- role: "user" | "assistant";
23
- content: string;
24
- sources: SourceChunk[];
25
- isStreaming?: boolean;
26
- }
27
-
28
  interface Props {
29
  activeDoc: DocInfo | null;
30
  onCitationClick: (page: number) => void;
31
  }
32
 
33
  export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
34
- const [messages, setMessages] = useState<ChatMsg[]>([]);
35
- const [input, setInput] = useState("");
36
- const [streaming, setStreaming] = useState(false);
37
- const [isTyping, setIsTyping] = useState(false);
 
 
 
 
 
38
  const [showExportMenu, setShowExportMenu] = useState(false);
39
  const textareaRef = useRef<HTMLTextAreaElement>(null);
40
  const bottomRef = useRef<HTMLDivElement>(null);
@@ -63,6 +53,12 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
63
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
64
  }, [messages]);
65
 
 
 
 
 
 
 
66
  // Load history on doc change
67
  useEffect(() => {
68
  if (!activeDoc) {
@@ -102,7 +98,7 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
102
  return () => {
103
  cancelled = true;
104
  };
105
- }, [activeDoc]);
106
 
107
  const handleSend = async () => {
108
  if (!input.trim() || streaming) return;
@@ -369,4 +365,4 @@ export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
369
  </div>
370
  </div>
371
  );
372
- }
 
3
  import { useState, useRef, useEffect } from "react";
4
  import type { DocInfo } from "@/app/dashboard/page";
5
  import { api, API_BASE } from "@/lib/api";
6
+ import { useChatStore, type ChatMsg, type SourceChunk } from "@/store/chat-store";
7
  import { Button } from "@/components/ui/button";
8
  import { Textarea } from "@/components/ui/textarea";
9
  import MessageBubble from "./MessageBubble";
10
  import SourceCard from "./SourceCard";
11
  import { Send, Loader2, Trash2, MessageSquare, Download } from "lucide-react";
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  interface Props {
14
  activeDoc: DocInfo | null;
15
  onCitationClick: (page: number) => void;
16
  }
17
 
18
  export default function ChatPanel({ activeDoc, onCitationClick }: Props) {
19
+ const messages = useChatStore((state) => state.messages);
20
+ const input = useChatStore((state) => state.input);
21
+ const streaming = useChatStore((state) => state.streaming);
22
+ const isTyping = useChatStore((state) => state.isTyping);
23
+ const setMessages = useChatStore((state) => state.setMessages);
24
+ const setInput = useChatStore((state) => state.setInput);
25
+ const setStreaming = useChatStore((state) => state.setStreaming);
26
+ const setIsTyping = useChatStore((state) => state.setIsTyping);
27
+ const resetChat = useChatStore((state) => state.resetChat);
28
  const [showExportMenu, setShowExportMenu] = useState(false);
29
  const textareaRef = useRef<HTMLTextAreaElement>(null);
30
  const bottomRef = useRef<HTMLDivElement>(null);
 
53
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
54
  }, [messages]);
55
 
56
+ useEffect(() => {
57
+ return () => {
58
+ resetChat();
59
+ };
60
+ }, [resetChat]);
61
+
62
  // Load history on doc change
63
  useEffect(() => {
64
  if (!activeDoc) {
 
98
  return () => {
99
  cancelled = true;
100
  };
101
+ }, [activeDoc, resetChat, setMessages]);
102
 
103
  const handleSend = async () => {
104
  if (!input.trim() || streaming) return;
 
365
  </div>
366
  </div>
367
  );
368
+ }
frontend/src/components/chat/MessageBubble.tsx CHANGED
@@ -3,7 +3,7 @@
3
  import { useState, useRef } from "react";
4
  import ReactMarkdown from "react-markdown";
5
  import remarkGfm from "remark-gfm";
6
- import type { ChatMsg } from "./ChatPanel";
7
  import { Brain, User, Copy, Check } from "lucide-react";
8
  import { Button } from "@/components/ui/button";
9
 
 
3
  import { useState, useRef } from "react";
4
  import ReactMarkdown from "react-markdown";
5
  import remarkGfm from "remark-gfm";
6
+ import type { ChatMsg } from "@/store/chat-store";
7
  import { Brain, User, Copy, Check } from "lucide-react";
8
  import { Button } from "@/components/ui/button";
9
 
frontend/src/components/chat/SourceCard.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import { useState } from "react";
4
- import type { SourceChunk } from "./ChatPanel";
5
  import { Badge } from "@/components/ui/badge";
6
  import { Button } from "@/components/ui/button";
7
  import { ChevronDown, ChevronUp, FileText, Eye } from "lucide-react";
 
1
  "use client";
2
 
3
  import { useState } from "react";
4
+ import type { SourceChunk } from "@/store/chat-store";
5
  import { Badge } from "@/components/ui/badge";
6
  import { Button } from "@/components/ui/button";
7
  import { ChevronDown, ChevronUp, FileText, Eye } from "lucide-react";
frontend/src/lib/auth.tsx CHANGED
@@ -1,73 +1,24 @@
1
  "use client";
2
 
3
- import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
4
- import { api } from "./api";
5
-
6
- interface User {
7
- id: string;
8
- username: string;
9
- email: string;
10
- is_admin: boolean;
11
- created_at: string;
12
- }
13
-
14
- interface AuthContextType {
15
- user: User | null;
16
- token: string | null;
17
- loading: boolean;
18
- login: (email: string, password: string) => Promise<void>;
19
- register: (username: string, email: string, password: string) => Promise<void>;
20
- logout: () => void;
21
- }
22
-
23
- const AuthContext = createContext<AuthContextType | undefined>(undefined);
24
 
25
  export function AuthProvider({ children }: { children: React.ReactNode }) {
26
- const [user, setUser] = useState<User | null>(null);
27
- // Lazy initializer reads localStorage once — avoids setState-in-effect lint error
28
- const [token, setToken] = useState<string | null>(
29
- () => (typeof window !== "undefined" ? localStorage.getItem("token") : null)
30
- );
31
- // loading=true only when a token exists and needs server validation.
32
- // If there's no token we're already done — no effect setState needed.
33
- const [loading, setLoading] = useState<boolean>(
34
- () => typeof window !== "undefined" && !!localStorage.getItem("token")
35
- );
36
 
37
- // ── Validate saved token on mount ─────────────────
38
- // NOTE: no synchronous setState here — setLoading/setUser/setToken are
39
- // only called inside async callbacks (.then / .catch / .finally).
40
  useEffect(() => {
41
- if (!token) return; // loading is already false when token is null
42
- api
43
- .get<User>("/api/v1/auth/me", { token })
44
- .then(setUser)
45
- .catch(() => {
46
- localStorage.removeItem("token");
47
- localStorage.removeItem("refresh_token");
48
- setToken(null);
49
- })
50
- .finally(() => setLoading(false));
51
- // eslint-disable-next-line react-hooks/exhaustive-deps
52
- }, []); // intentionally runs once on mount only
53
 
54
- // ── Listen for token refresh events from ApiClient ──
55
- // When the API client auto-refreshes tokens, it dispatches custom events
56
- // so this context stays in sync without prop drilling.
57
  useEffect(() => {
58
  const handleTokensRefreshed = (e: Event) => {
59
- const detail = (e as CustomEvent).detail;
60
- if (detail?.accessToken) {
61
- setToken(detail.accessToken);
62
- }
63
- if (detail?.user) {
64
- setUser(detail.user);
65
- }
66
  };
67
 
68
  const handleLoggedOut = () => {
69
- setToken(null);
70
- setUser(null);
71
  };
72
 
73
  window.addEventListener("auth:tokens-refreshed", handleTokensRefreshed);
@@ -77,46 +28,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
77
  window.removeEventListener("auth:tokens-refreshed", handleTokensRefreshed);
78
  window.removeEventListener("auth:logged-out", handleLoggedOut);
79
  };
80
- }, []);
81
-
82
- const login = useCallback(async (email: string, password: string) => {
83
- const data = await api.post<{ access_token: string; refresh_token: string; user: User }>(
84
- "/api/v1/auth/login",
85
- { email, password }
86
- );
87
- localStorage.setItem("token", data.access_token);
88
- localStorage.setItem("refresh_token", data.refresh_token);
89
- setToken(data.access_token);
90
- setUser(data.user);
91
- }, []);
92
-
93
- const register = useCallback(async (username: string, email: string, password: string) => {
94
- const data = await api.post<{ access_token: string; refresh_token: string; user: User }>(
95
- "/api/v1/auth/register",
96
- { username, email, password }
97
- );
98
- localStorage.setItem("token", data.access_token);
99
- localStorage.setItem("refresh_token", data.refresh_token);
100
- setToken(data.access_token);
101
- setUser(data.user);
102
- }, []);
103
-
104
- const logout = useCallback(() => {
105
- localStorage.removeItem("token");
106
- localStorage.removeItem("refresh_token");
107
- setToken(null);
108
- setUser(null);
109
- }, []);
110
 
111
- return (
112
- <AuthContext.Provider value={{ user, token, loading, login, register, logout }}>
113
- {children}
114
- </AuthContext.Provider>
115
- );
116
  }
117
 
118
  export function useAuth() {
119
- const ctx = useContext(AuthContext);
120
- if (!ctx) throw new Error("useAuth must be used within AuthProvider");
121
- return ctx;
122
  }
 
1
  "use client";
2
 
3
+ import React, { useEffect } from "react";
4
+ import { useAuthStore } from "@/store/auth-store";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  export function AuthProvider({ children }: { children: React.ReactNode }) {
7
+ const initializeAuth = useAuthStore((state) => state.initializeAuth);
8
+ const syncTokensRefreshed = useAuthStore((state) => state.syncTokensRefreshed);
9
+ const syncLoggedOut = useAuthStore((state) => state.syncLoggedOut);
 
 
 
 
 
 
 
10
 
 
 
 
11
  useEffect(() => {
12
+ void initializeAuth();
13
+ }, [initializeAuth]);
 
 
 
 
 
 
 
 
 
 
14
 
 
 
 
15
  useEffect(() => {
16
  const handleTokensRefreshed = (e: Event) => {
17
+ syncTokensRefreshed((e as CustomEvent).detail);
 
 
 
 
 
 
18
  };
19
 
20
  const handleLoggedOut = () => {
21
+ syncLoggedOut();
 
22
  };
23
 
24
  window.addEventListener("auth:tokens-refreshed", handleTokensRefreshed);
 
28
  window.removeEventListener("auth:tokens-refreshed", handleTokensRefreshed);
29
  window.removeEventListener("auth:logged-out", handleLoggedOut);
30
  };
31
+ }, [syncLoggedOut, syncTokensRefreshed]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ return <>{children}</>;
 
 
 
 
34
  }
35
 
36
  export function useAuth() {
37
+ return useAuthStore();
 
 
38
  }
frontend/src/store/auth-store.ts ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+ import { api } from "@/lib/api";
5
+
6
+ export interface AuthUser {
7
+ id: string;
8
+ username: string;
9
+ email: string;
10
+ is_admin: boolean;
11
+ created_at: string;
12
+ }
13
+
14
+ interface AuthStore {
15
+ user: AuthUser | null;
16
+ token: string | null;
17
+ loading: boolean;
18
+ initialized: boolean;
19
+ login: (email: string, password: string) => Promise<void>;
20
+ register: (username: string, email: string, password: string) => Promise<void>;
21
+ logout: () => void;
22
+ initializeAuth: () => Promise<void>;
23
+ syncTokensRefreshed: (detail?: { accessToken?: string; user?: AuthUser | null }) => void;
24
+ syncLoggedOut: () => void;
25
+ }
26
+
27
+ const getStoredToken = () =>
28
+ typeof window !== "undefined" ? localStorage.getItem("token") : null;
29
+
30
+ const clearStoredTokens = () => {
31
+ if (typeof window === "undefined") return;
32
+ localStorage.removeItem("token");
33
+ localStorage.removeItem("refresh_token");
34
+ };
35
+
36
+ export const useAuthStore = create<AuthStore>((set, get) => ({
37
+ user: null,
38
+ token: getStoredToken(),
39
+ loading: !!getStoredToken(),
40
+ initialized: false,
41
+
42
+ async login(email, password) {
43
+ const data = await api.post<{ access_token: string; refresh_token: string; user: AuthUser }>(
44
+ "/api/v1/auth/login",
45
+ { email, password }
46
+ );
47
+
48
+ localStorage.setItem("token", data.access_token);
49
+ localStorage.setItem("refresh_token", data.refresh_token);
50
+ set({
51
+ token: data.access_token,
52
+ user: data.user,
53
+ loading: false,
54
+ initialized: true,
55
+ });
56
+ },
57
+
58
+ async register(username, email, password) {
59
+ const data = await api.post<{ access_token: string; refresh_token: string; user: AuthUser }>(
60
+ "/api/v1/auth/register",
61
+ { username, email, password }
62
+ );
63
+
64
+ localStorage.setItem("token", data.access_token);
65
+ localStorage.setItem("refresh_token", data.refresh_token);
66
+ set({
67
+ token: data.access_token,
68
+ user: data.user,
69
+ loading: false,
70
+ initialized: true,
71
+ });
72
+ },
73
+
74
+ logout() {
75
+ clearStoredTokens();
76
+ set({
77
+ token: null,
78
+ user: null,
79
+ loading: false,
80
+ initialized: true,
81
+ });
82
+ },
83
+
84
+ async initializeAuth() {
85
+ const { initialized, token } = get();
86
+ if (initialized) return;
87
+
88
+ const storedToken = token ?? getStoredToken();
89
+ if (!storedToken) {
90
+ set({ token: null, user: null, loading: false, initialized: true });
91
+ return;
92
+ }
93
+
94
+ set({ token: storedToken, loading: true });
95
+
96
+ try {
97
+ const user = await api.get<AuthUser>("/api/v1/auth/me", { token: storedToken });
98
+ set({ user, token: storedToken, loading: false, initialized: true });
99
+ } catch {
100
+ clearStoredTokens();
101
+ set({ user: null, token: null, loading: false, initialized: true });
102
+ }
103
+ },
104
+
105
+ syncTokensRefreshed(detail) {
106
+ if (!detail) return;
107
+
108
+ set((state) => ({
109
+ token: detail.accessToken ?? state.token,
110
+ user: detail.user ?? state.user,
111
+ loading: false,
112
+ initialized: true,
113
+ }));
114
+ },
115
+
116
+ syncLoggedOut() {
117
+ set({
118
+ token: null,
119
+ user: null,
120
+ loading: false,
121
+ initialized: true,
122
+ });
123
+ },
124
+ }));
frontend/src/store/chat-store.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { create } from "zustand";
4
+
5
+ export interface SourceChunk {
6
+ text: string;
7
+ filename: string;
8
+ page: number;
9
+ score: number;
10
+ confidence: number;
11
+ }
12
+
13
+ export interface ChatMsg {
14
+ id: string;
15
+ role: "user" | "assistant";
16
+ content: string;
17
+ sources: SourceChunk[];
18
+ isStreaming?: boolean;
19
+ }
20
+
21
+ type Setter<T> = T | ((prev: T) => T);
22
+
23
+ interface ChatStore {
24
+ messages: ChatMsg[];
25
+ input: string;
26
+ streaming: boolean;
27
+ isTyping: boolean;
28
+ setMessages: (value: Setter<ChatMsg[]>) => void;
29
+ setInput: (value: Setter<string>) => void;
30
+ setStreaming: (value: Setter<boolean>) => void;
31
+ setIsTyping: (value: Setter<boolean>) => void;
32
+ resetChat: () => void;
33
+ }
34
+
35
+ const resolveValue = <T,>(value: Setter<T>, current: T): T =>
36
+ typeof value === "function" ? (value as (prev: T) => T)(current) : value;
37
+
38
+ export const useChatStore = create<ChatStore>((set) => ({
39
+ messages: [],
40
+ input: "",
41
+ streaming: false,
42
+ isTyping: false,
43
+
44
+ setMessages(value) {
45
+ set((state) => ({ messages: resolveValue(value, state.messages) }));
46
+ },
47
+
48
+ setInput(value) {
49
+ set((state) => ({ input: resolveValue(value, state.input) }));
50
+ },
51
+
52
+ setStreaming(value) {
53
+ set((state) => ({ streaming: resolveValue(value, state.streaming) }));
54
+ },
55
+
56
+ setIsTyping(value) {
57
+ set((state) => ({ isTyping: resolveValue(value, state.isTyping) }));
58
+ },
59
+
60
+ resetChat() {
61
+ set({
62
+ messages: [],
63
+ input: "",
64
+ streaming: false,
65
+ isTyping: false,
66
+ });
67
+ },
68
+ }));