Soham Waghmare commited on
Commit
780df80
·
1 Parent(s): 2d040a7

feat: mvp frontend

Browse files
Files changed (44) hide show
  1. .gitignore +4 -0
  2. KnowledgeNet.excalidraw +2193 -20
  3. backend/.gitignore +0 -1
  4. backend/app.py +10 -15
  5. backend/knet.py +131 -90
  6. backend/output.json +0 -0
  7. frontend/bun.lock +0 -0
  8. frontend/components.json +21 -0
  9. frontend/package.json +27 -3
  10. frontend/src/app/fonts/GeistMonoVF.woff +0 -0
  11. frontend/src/app/fonts/GeistVF.woff +0 -0
  12. frontend/src/app/globals.css +67 -15
  13. frontend/src/app/layout.tsx +9 -24
  14. frontend/src/app/page.tsx +3 -3
  15. frontend/src/components/ChatHistory.tsx +63 -0
  16. frontend/src/components/ChatInterface.tsx +274 -0
  17. frontend/src/components/Message.tsx +97 -0
  18. frontend/src/components/MessageInput.tsx +75 -0
  19. frontend/src/components/ResearchControls.tsx +73 -0
  20. frontend/src/components/theme-provider.tsx +8 -0
  21. frontend/src/components/ui/ChatLayout.tsx +85 -0
  22. frontend/src/components/ui/ConversationList.tsx +52 -0
  23. frontend/src/components/ui/ThemeToggle.tsx +34 -0
  24. frontend/src/components/ui/alert.tsx +27 -0
  25. frontend/src/components/ui/avatar.tsx +50 -0
  26. frontend/src/components/ui/button.tsx +57 -0
  27. frontend/src/components/ui/card.tsx +76 -0
  28. frontend/src/components/ui/checkbox.tsx +30 -0
  29. frontend/src/components/ui/dialog.tsx +122 -0
  30. frontend/src/components/ui/dropdown-menu.tsx +200 -0
  31. frontend/src/components/ui/input.tsx +22 -0
  32. frontend/src/components/ui/label.tsx +26 -0
  33. frontend/src/components/ui/resizable.tsx +45 -0
  34. frontend/src/components/ui/scroll-area.tsx +24 -0
  35. frontend/src/components/ui/select.tsx +159 -0
  36. frontend/src/components/ui/separator.tsx +31 -0
  37. frontend/src/components/ui/sheet.tsx +140 -0
  38. frontend/src/components/ui/tabs.tsx +22 -0
  39. frontend/src/components/ui/textarea.tsx +95 -0
  40. frontend/src/components/ui/tooltip.tsx +32 -0
  41. frontend/src/lib/socket.ts +30 -0
  42. frontend/src/lib/types.ts +60 -0
  43. frontend/src/lib/utils.ts +6 -0
  44. frontend/tailwind.config.ts +52 -8
.gitignore CHANGED
@@ -1,3 +1,5 @@
 
 
1
  # Flask ignore files
2
  backend/__pycache__/
3
  backend/*.pyc
@@ -6,6 +8,8 @@ backend/*.pyd
6
  backend/*.pyo
7
  backend/.venv/
8
  backend/.env*
 
 
9
 
10
  # Next.js ignore files
11
  # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
1
+ **/.env
2
+
3
  # Flask ignore files
4
  backend/__pycache__/
5
  backend/*.pyc
 
8
  backend/*.pyo
9
  backend/.venv/
10
  backend/.env*
11
+ backend/downloads/*
12
+ backend/output.json
13
 
14
  # Next.js ignore files
15
  # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
KnowledgeNet.excalidraw CHANGED
@@ -3990,7 +3990,7 @@
3990
  "version": 92,
3991
  "versionNonce": 1221535793,
3992
  "isDeleted": false,
3993
- "boundElements": null,
3994
  "updated": 1740410117898,
3995
  "link": null,
3996
  "locked": false,
@@ -4005,10 +4005,10 @@
4005
  "lineHeight": 1.25
4006
  },
4007
  {
4008
- "id": "QwQpbmMZYUJCdK5IFIkvr",
4009
  "type": "text",
4010
- "x": 3952.7474993968267,
4011
- "y": -357.5833329999999,
4012
  "width": 727.5238647460938,
4013
  "height": 105,
4014
  "angle": 0,
@@ -4023,12 +4023,12 @@
4023
  "frameId": null,
4024
  "index": "b0I",
4025
  "roundness": null,
4026
- "seed": 925609498,
4027
- "version": 342,
4028
- "versionNonce": 71133361,
4029
  "isDeleted": false,
4030
  "boundElements": [],
4031
- "updated": 1740410131106,
4032
  "link": null,
4033
  "locked": false,
4034
  "text": "Use Case Diagram",
@@ -4111,7 +4111,7 @@
4111
  "version": 166,
4112
  "versionNonce": 854836337,
4113
  "isDeleted": false,
4114
- "boundElements": null,
4115
  "updated": 1740410330502,
4116
  "link": null,
4117
  "locked": false,
@@ -4187,7 +4187,7 @@
4187
  "version": 24,
4188
  "versionNonce": 1896009777,
4189
  "isDeleted": false,
4190
- "boundElements": null,
4191
  "updated": 1740410209932,
4192
  "link": null,
4193
  "locked": false,
@@ -4279,7 +4279,7 @@
4279
  "version": 121,
4280
  "versionNonce": 628335089,
4281
  "isDeleted": false,
4282
- "boundElements": null,
4283
  "updated": 1740410219593,
4284
  "link": null,
4285
  "locked": false,
@@ -4355,7 +4355,7 @@
4355
  "version": 261,
4356
  "versionNonce": 481340991,
4357
  "isDeleted": false,
4358
- "boundElements": null,
4359
  "updated": 1740410234474,
4360
  "link": null,
4361
  "locked": false,
@@ -4443,7 +4443,7 @@
4443
  "version": 295,
4444
  "versionNonce": 86343071,
4445
  "isDeleted": false,
4446
- "boundElements": null,
4447
  "updated": 1740410412315,
4448
  "link": null,
4449
  "locked": false,
@@ -4480,7 +4480,7 @@
4480
  "version": 85,
4481
  "versionNonce": 745306257,
4482
  "isDeleted": false,
4483
- "boundElements": null,
4484
  "updated": 1740410342893,
4485
  "link": null,
4486
  "locked": false,
@@ -4551,7 +4551,7 @@
4551
  "version": 64,
4552
  "versionNonce": 1485153297,
4553
  "isDeleted": false,
4554
- "boundElements": null,
4555
  "updated": 1740410346276,
4556
  "link": null,
4557
  "locked": false,
@@ -4614,7 +4614,7 @@
4614
  "version": 115,
4615
  "versionNonce": 1416571167,
4616
  "isDeleted": false,
4617
- "boundElements": null,
4618
  "updated": 1740410351698,
4619
  "link": null,
4620
  "locked": false,
@@ -4685,7 +4685,7 @@
4685
  "version": 113,
4686
  "versionNonce": 694937969,
4687
  "isDeleted": false,
4688
- "boundElements": null,
4689
  "updated": 1740410359339,
4690
  "link": null,
4691
  "locked": false,
@@ -4756,7 +4756,7 @@
4756
  "version": 99,
4757
  "versionNonce": 1591321329,
4758
  "isDeleted": false,
4759
- "boundElements": null,
4760
  "updated": 1740410373406,
4761
  "link": null,
4762
  "locked": false,
@@ -4827,7 +4827,7 @@
4827
  "version": 105,
4828
  "versionNonce": 1997100959,
4829
  "isDeleted": false,
4830
- "boundElements": null,
4831
  "updated": 1740410388128,
4832
  "link": null,
4833
  "locked": false,
@@ -4898,7 +4898,7 @@
4898
  "version": 44,
4899
  "versionNonce": 1743458015,
4900
  "isDeleted": false,
4901
- "boundElements": null,
4902
  "updated": 1740410394139,
4903
  "link": null,
4904
  "locked": false,
@@ -4945,6 +4945,2179 @@
4945
  "fixedSegments": null,
4946
  "startIsSpecial": null,
4947
  "endIsSpecial": null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4948
  }
4949
  ],
4950
  "appState": {
 
3990
  "version": 92,
3991
  "versionNonce": 1221535793,
3992
  "isDeleted": false,
3993
+ "boundElements": [],
3994
  "updated": 1740410117898,
3995
  "link": null,
3996
  "locked": false,
 
4005
  "lineHeight": 1.25
4006
  },
4007
  {
4008
+ "id": "Hd85L_o2whuQ37yZPmET7",
4009
  "type": "text",
4010
+ "x": 3834.7474993968267,
4011
+ "y": -385.5833329999999,
4012
  "width": 727.5238647460938,
4013
  "height": 105,
4014
  "angle": 0,
 
4023
  "frameId": null,
4024
  "index": "b0I",
4025
  "roundness": null,
4026
+ "seed": 1112393545,
4027
+ "version": 573,
4028
+ "versionNonce": 1303852039,
4029
  "isDeleted": false,
4030
  "boundElements": [],
4031
+ "updated": 1741576813086,
4032
  "link": null,
4033
  "locked": false,
4034
  "text": "Use Case Diagram",
 
4111
  "version": 166,
4112
  "versionNonce": 854836337,
4113
  "isDeleted": false,
4114
+ "boundElements": [],
4115
  "updated": 1740410330502,
4116
  "link": null,
4117
  "locked": false,
 
4187
  "version": 24,
4188
  "versionNonce": 1896009777,
4189
  "isDeleted": false,
4190
+ "boundElements": [],
4191
  "updated": 1740410209932,
4192
  "link": null,
4193
  "locked": false,
 
4279
  "version": 121,
4280
  "versionNonce": 628335089,
4281
  "isDeleted": false,
4282
+ "boundElements": [],
4283
  "updated": 1740410219593,
4284
  "link": null,
4285
  "locked": false,
 
4355
  "version": 261,
4356
  "versionNonce": 481340991,
4357
  "isDeleted": false,
4358
+ "boundElements": [],
4359
  "updated": 1740410234474,
4360
  "link": null,
4361
  "locked": false,
 
4443
  "version": 295,
4444
  "versionNonce": 86343071,
4445
  "isDeleted": false,
4446
+ "boundElements": [],
4447
  "updated": 1740410412315,
4448
  "link": null,
4449
  "locked": false,
 
4480
  "version": 85,
4481
  "versionNonce": 745306257,
4482
  "isDeleted": false,
4483
+ "boundElements": [],
4484
  "updated": 1740410342893,
4485
  "link": null,
4486
  "locked": false,
 
4551
  "version": 64,
4552
  "versionNonce": 1485153297,
4553
  "isDeleted": false,
4554
+ "boundElements": [],
4555
  "updated": 1740410346276,
4556
  "link": null,
4557
  "locked": false,
 
4614
  "version": 115,
4615
  "versionNonce": 1416571167,
4616
  "isDeleted": false,
4617
+ "boundElements": [],
4618
  "updated": 1740410351698,
4619
  "link": null,
4620
  "locked": false,
 
4685
  "version": 113,
4686
  "versionNonce": 694937969,
4687
  "isDeleted": false,
4688
+ "boundElements": [],
4689
  "updated": 1740410359339,
4690
  "link": null,
4691
  "locked": false,
 
4756
  "version": 99,
4757
  "versionNonce": 1591321329,
4758
  "isDeleted": false,
4759
+ "boundElements": [],
4760
  "updated": 1740410373406,
4761
  "link": null,
4762
  "locked": false,
 
4827
  "version": 105,
4828
  "versionNonce": 1997100959,
4829
  "isDeleted": false,
4830
+ "boundElements": [],
4831
  "updated": 1740410388128,
4832
  "link": null,
4833
  "locked": false,
 
4898
  "version": 44,
4899
  "versionNonce": 1743458015,
4900
  "isDeleted": false,
4901
+ "boundElements": [],
4902
  "updated": 1740410394139,
4903
  "link": null,
4904
  "locked": false,
 
4945
  "fixedSegments": null,
4946
  "startIsSpecial": null,
4947
  "endIsSpecial": null
4948
+ },
4949
+ {
4950
+ "id": "_DXsWPZLu9Lg_N3cYUbAP",
4951
+ "type": "rectangle",
4952
+ "x": 6143.606589566099,
4953
+ "y": -106.7448757200093,
4954
+ "width": 1541.4296875,
4955
+ "height": 768.1015948763029,
4956
+ "angle": 0,
4957
+ "strokeColor": "#1e1e1e",
4958
+ "backgroundColor": "transparent",
4959
+ "fillStyle": "solid",
4960
+ "strokeWidth": 2,
4961
+ "strokeStyle": "solid",
4962
+ "roughness": 1,
4963
+ "opacity": 100,
4964
+ "groupIds": [
4965
+ "wjqPa5lw_KjJSGahompw4"
4966
+ ],
4967
+ "frameId": null,
4968
+ "index": "b1c",
4969
+ "roundness": null,
4970
+ "seed": 2136851817,
4971
+ "version": 92,
4972
+ "versionNonce": 750499017,
4973
+ "isDeleted": false,
4974
+ "boundElements": [],
4975
+ "updated": 1741576957668,
4976
+ "link": null,
4977
+ "locked": false
4978
+ },
4979
+ {
4980
+ "id": "hh-vJczGSvSCpkhjc-ZFd",
4981
+ "type": "rectangle",
4982
+ "x": 6884.735501342112,
4983
+ "y": 712.7366477728008,
4984
+ "width": 64.6875,
4985
+ "height": 44,
4986
+ "angle": 0,
4987
+ "strokeColor": "#1e1e1e",
4988
+ "backgroundColor": "transparent",
4989
+ "fillStyle": "solid",
4990
+ "strokeWidth": 2,
4991
+ "strokeStyle": "solid",
4992
+ "roughness": 1,
4993
+ "opacity": 100,
4994
+ "groupIds": [],
4995
+ "frameId": null,
4996
+ "index": "b1d",
4997
+ "roundness": null,
4998
+ "seed": 808661065,
4999
+ "version": 154,
5000
+ "versionNonce": 1545689785,
5001
+ "isDeleted": false,
5002
+ "boundElements": [
5003
+ {
5004
+ "type": "text",
5005
+ "id": "zSH_0RVoAH6FqvlC-qOEe"
5006
+ },
5007
+ {
5008
+ "id": "uuVJhiL1l0abY9oNZiD10",
5009
+ "type": "arrow"
5010
+ },
5011
+ {
5012
+ "id": "RG4hRN73PDSPg9TvjEABI",
5013
+ "type": "arrow"
5014
+ }
5015
+ ],
5016
+ "updated": 1741577112903,
5017
+ "link": null,
5018
+ "locked": false
5019
+ },
5020
+ {
5021
+ "id": "5rQwEFQ0OLt7lCCh6ROrl",
5022
+ "type": "rectangle",
5023
+ "x": 6816.856929026729,
5024
+ "y": -77.24788666946003,
5025
+ "width": 245.24902833716055,
5026
+ "height": 51.91470072896671,
5027
+ "angle": 0,
5028
+ "strokeColor": "#1e1e1e",
5029
+ "backgroundColor": "transparent",
5030
+ "fillStyle": "solid",
5031
+ "strokeWidth": 2,
5032
+ "strokeStyle": "solid",
5033
+ "roughness": 1,
5034
+ "opacity": 100,
5035
+ "groupIds": [
5036
+ "wjqPa5lw_KjJSGahompw4"
5037
+ ],
5038
+ "frameId": null,
5039
+ "index": "b1e",
5040
+ "roundness": null,
5041
+ "seed": 1020804905,
5042
+ "version": 101,
5043
+ "versionNonce": 692196601,
5044
+ "isDeleted": false,
5045
+ "boundElements": [
5046
+ {
5047
+ "type": "text",
5048
+ "id": "YN7Z_MhrnVfus-HHFPTk1"
5049
+ },
5050
+ {
5051
+ "id": "Gr_IecqseQ5STfEBPzVML",
5052
+ "type": "arrow"
5053
+ },
5054
+ {
5055
+ "id": "uuVJhiL1l0abY9oNZiD10",
5056
+ "type": "arrow"
5057
+ }
5058
+ ],
5059
+ "updated": 1741577060675,
5060
+ "link": null,
5061
+ "locked": false
5062
+ },
5063
+ {
5064
+ "id": "lcNOx1PJ1LukZ3eN5WcGs",
5065
+ "type": "rectangle",
5066
+ "x": 6658.135474507539,
5067
+ "y": 33.66079216060521,
5068
+ "width": 184.00590482095765,
5069
+ "height": 51.91470072896671,
5070
+ "angle": 0,
5071
+ "strokeColor": "#1e1e1e",
5072
+ "backgroundColor": "transparent",
5073
+ "fillStyle": "solid",
5074
+ "strokeWidth": 2,
5075
+ "strokeStyle": "solid",
5076
+ "roughness": 1,
5077
+ "opacity": 100,
5078
+ "groupIds": [
5079
+ "wjqPa5lw_KjJSGahompw4"
5080
+ ],
5081
+ "frameId": null,
5082
+ "index": "b1f",
5083
+ "roundness": null,
5084
+ "seed": 1404596745,
5085
+ "version": 104,
5086
+ "versionNonce": 210016041,
5087
+ "isDeleted": false,
5088
+ "boundElements": [
5089
+ {
5090
+ "type": "text",
5091
+ "id": "A45yyUfo1lPMEY8TmwNAv"
5092
+ },
5093
+ {
5094
+ "id": "Gr_IecqseQ5STfEBPzVML",
5095
+ "type": "arrow"
5096
+ },
5097
+ {
5098
+ "id": "jXazvlSAB2utF1woHHpxJ",
5099
+ "type": "arrow"
5100
+ },
5101
+ {
5102
+ "id": "3raa-F9gTwZ7ZG2sCiVMU",
5103
+ "type": "arrow"
5104
+ }
5105
+ ],
5106
+ "updated": 1741576957669,
5107
+ "link": null,
5108
+ "locked": false
5109
+ },
5110
+ {
5111
+ "id": "YPquWrf902Bx_VzPe5ZKo",
5112
+ "type": "rectangle",
5113
+ "x": 6733.988825412842,
5114
+ "y": 144.56947099067045,
5115
+ "width": 308.5753767050585,
5116
+ "height": 51.91470072896671,
5117
+ "angle": 0,
5118
+ "strokeColor": "#1e1e1e",
5119
+ "backgroundColor": "transparent",
5120
+ "fillStyle": "solid",
5121
+ "strokeWidth": 2,
5122
+ "strokeStyle": "solid",
5123
+ "roughness": 1,
5124
+ "opacity": 100,
5125
+ "groupIds": [
5126
+ "wjqPa5lw_KjJSGahompw4"
5127
+ ],
5128
+ "frameId": null,
5129
+ "index": "b1g",
5130
+ "roundness": null,
5131
+ "seed": 1238321385,
5132
+ "version": 108,
5133
+ "versionNonce": 1116994953,
5134
+ "isDeleted": false,
5135
+ "boundElements": [
5136
+ {
5137
+ "type": "text",
5138
+ "id": "fXbbtx_NcVZBC_fsO1Uff"
5139
+ },
5140
+ {
5141
+ "id": "jXazvlSAB2utF1woHHpxJ",
5142
+ "type": "arrow"
5143
+ }
5144
+ ],
5145
+ "updated": 1741576957669,
5146
+ "link": null,
5147
+ "locked": false
5148
+ },
5149
+ {
5150
+ "id": "O4ynYwner8EHSv-cUHyx0",
5151
+ "type": "rectangle",
5152
+ "x": 6653.296124741433,
5153
+ "y": 289.69465711937283,
5154
+ "width": 469.9607780478762,
5155
+ "height": 51.91470072896671,
5156
+ "angle": 0,
5157
+ "strokeColor": "#1e1e1e",
5158
+ "backgroundColor": "transparent",
5159
+ "fillStyle": "solid",
5160
+ "strokeWidth": 2,
5161
+ "strokeStyle": "solid",
5162
+ "roughness": 1,
5163
+ "opacity": 100,
5164
+ "groupIds": [
5165
+ "wjqPa5lw_KjJSGahompw4"
5166
+ ],
5167
+ "frameId": null,
5168
+ "index": "b1h",
5169
+ "roundness": null,
5170
+ "seed": 1218986953,
5171
+ "version": 112,
5172
+ "versionNonce": 199055049,
5173
+ "isDeleted": false,
5174
+ "boundElements": [
5175
+ {
5176
+ "type": "text",
5177
+ "id": "V3UN0NpFvZDTX16OkuBNR"
5178
+ },
5179
+ {
5180
+ "id": "hktnzSdCZsg2XG9eDy1n-",
5181
+ "type": "arrow"
5182
+ },
5183
+ {
5184
+ "id": "fMbl9KWZiggWCvF5xaMRT",
5185
+ "type": "arrow"
5186
+ },
5187
+ {
5188
+ "id": "QTMS_iw6t6ynVYi-LOM0h",
5189
+ "type": "arrow"
5190
+ },
5191
+ {
5192
+ "id": "FCEWIaj7DywBOG0ukJrRU",
5193
+ "type": "arrow"
5194
+ },
5195
+ {
5196
+ "id": "vZM--SBjM4zyowOgs3hDv",
5197
+ "type": "arrow"
5198
+ }
5199
+ ],
5200
+ "updated": 1741576992554,
5201
+ "link": null,
5202
+ "locked": false
5203
+ },
5204
+ {
5205
+ "id": "d-Obq1jyhqw7abPHd6_i5",
5206
+ "type": "rectangle",
5207
+ "x": 6233.346569847856,
5208
+ "y": 434.81984324807524,
5209
+ "width": 220.0659739352541,
5210
+ "height": 51.91470072896671,
5211
+ "angle": 0,
5212
+ "strokeColor": "#1e1e1e",
5213
+ "backgroundColor": "transparent",
5214
+ "fillStyle": "solid",
5215
+ "strokeWidth": 2,
5216
+ "strokeStyle": "solid",
5217
+ "roughness": 1,
5218
+ "opacity": 100,
5219
+ "groupIds": [
5220
+ "wjqPa5lw_KjJSGahompw4"
5221
+ ],
5222
+ "frameId": null,
5223
+ "index": "b1i",
5224
+ "roundness": null,
5225
+ "seed": 1012851369,
5226
+ "version": 113,
5227
+ "versionNonce": 1896293513,
5228
+ "isDeleted": false,
5229
+ "boundElements": [
5230
+ {
5231
+ "type": "text",
5232
+ "id": "lKO9DytZCWjoMyMUO9WnK"
5233
+ },
5234
+ {
5235
+ "id": "3raa-F9gTwZ7ZG2sCiVMU",
5236
+ "type": "arrow"
5237
+ },
5238
+ {
5239
+ "id": "fMbl9KWZiggWCvF5xaMRT",
5240
+ "type": "arrow"
5241
+ }
5242
+ ],
5243
+ "updated": 1741576957670,
5244
+ "link": null,
5245
+ "locked": false
5246
+ },
5247
+ {
5248
+ "id": "SLQjJ54OvcwHKgE0saBYW",
5249
+ "type": "rectangle",
5250
+ "x": 6553.568648323342,
5251
+ "y": 434.81984324807524,
5252
+ "width": 298.06707435580034,
5253
+ "height": 51.91470072896671,
5254
+ "angle": 0,
5255
+ "strokeColor": "#1e1e1e",
5256
+ "backgroundColor": "transparent",
5257
+ "fillStyle": "solid",
5258
+ "strokeWidth": 2,
5259
+ "strokeStyle": "solid",
5260
+ "roughness": 1,
5261
+ "opacity": 100,
5262
+ "groupIds": [
5263
+ "wjqPa5lw_KjJSGahompw4"
5264
+ ],
5265
+ "frameId": null,
5266
+ "index": "b1j",
5267
+ "roundness": null,
5268
+ "seed": 27174281,
5269
+ "version": 106,
5270
+ "versionNonce": 448796681,
5271
+ "isDeleted": false,
5272
+ "boundElements": [
5273
+ {
5274
+ "type": "text",
5275
+ "id": "ZIBlXFowG3rpzs7ZAmiZZ"
5276
+ },
5277
+ {
5278
+ "id": "QTMS_iw6t6ynVYi-LOM0h",
5279
+ "type": "arrow"
5280
+ }
5281
+ ],
5282
+ "updated": 1741576957670,
5283
+ "link": null,
5284
+ "locked": false
5285
+ },
5286
+ {
5287
+ "id": "SmhmRdO5OootjWvVdhZVC",
5288
+ "type": "rectangle",
5289
+ "x": 6910.6297007802405,
5290
+ "y": 434.81984324807524,
5291
+ "width": 326.64228249851993,
5292
+ "height": 51.91470072896671,
5293
+ "angle": 0,
5294
+ "strokeColor": "#1e1e1e",
5295
+ "backgroundColor": "transparent",
5296
+ "fillStyle": "solid",
5297
+ "strokeWidth": 2,
5298
+ "strokeStyle": "solid",
5299
+ "roughness": 1,
5300
+ "opacity": 100,
5301
+ "groupIds": [
5302
+ "wjqPa5lw_KjJSGahompw4"
5303
+ ],
5304
+ "frameId": null,
5305
+ "index": "b1k",
5306
+ "roundness": null,
5307
+ "seed": 1403194473,
5308
+ "version": 104,
5309
+ "versionNonce": 1977506121,
5310
+ "isDeleted": false,
5311
+ "boundElements": [
5312
+ {
5313
+ "type": "text",
5314
+ "id": "avhHlotKdqzZ5gQytgcwQ"
5315
+ },
5316
+ {
5317
+ "id": "FCEWIaj7DywBOG0ukJrRU",
5318
+ "type": "arrow"
5319
+ }
5320
+ ],
5321
+ "updated": 1741576964302,
5322
+ "link": null,
5323
+ "locked": false
5324
+ },
5325
+ {
5326
+ "id": "-AKHuCF8e7h2_yxWV-Lgt",
5327
+ "type": "rectangle",
5328
+ "x": 7406.898105937576,
5329
+ "y": 434.81984324807524,
5330
+ "width": 236.842386457754,
5331
+ "height": 51.91470072896671,
5332
+ "angle": 0,
5333
+ "strokeColor": "#1e1e1e",
5334
+ "backgroundColor": "transparent",
5335
+ "fillStyle": "solid",
5336
+ "strokeWidth": 2,
5337
+ "strokeStyle": "solid",
5338
+ "roughness": 1,
5339
+ "opacity": 100,
5340
+ "groupIds": [
5341
+ "wjqPa5lw_KjJSGahompw4"
5342
+ ],
5343
+ "frameId": null,
5344
+ "index": "b1l",
5345
+ "roundness": null,
5346
+ "seed": 2050463561,
5347
+ "version": 109,
5348
+ "versionNonce": 928298601,
5349
+ "isDeleted": false,
5350
+ "boundElements": [
5351
+ {
5352
+ "type": "text",
5353
+ "id": "GHHkKHEJHxyvjEsluWanq"
5354
+ },
5355
+ {
5356
+ "id": "vZM--SBjM4zyowOgs3hDv",
5357
+ "type": "arrow"
5358
+ },
5359
+ {
5360
+ "id": "F704_xUi4glbWWo1UMSZD",
5361
+ "type": "arrow"
5362
+ }
5363
+ ],
5364
+ "updated": 1741577012386,
5365
+ "link": null,
5366
+ "locked": false
5367
+ },
5368
+ {
5369
+ "id": "W854nPSz0p4lut302JyEo",
5370
+ "type": "rectangle",
5371
+ "x": 6547.033221686829,
5372
+ "y": 579.9450293767776,
5373
+ "width": 311.13792762882497,
5374
+ "height": 51.91470072896671,
5375
+ "angle": 0,
5376
+ "strokeColor": "#1e1e1e",
5377
+ "backgroundColor": "transparent",
5378
+ "fillStyle": "solid",
5379
+ "strokeWidth": 2,
5380
+ "strokeStyle": "solid",
5381
+ "roughness": 1,
5382
+ "opacity": 100,
5383
+ "groupIds": [
5384
+ "wjqPa5lw_KjJSGahompw4"
5385
+ ],
5386
+ "frameId": null,
5387
+ "index": "b1m",
5388
+ "roundness": null,
5389
+ "seed": 934790697,
5390
+ "version": 107,
5391
+ "versionNonce": 1443202297,
5392
+ "isDeleted": false,
5393
+ "boundElements": [
5394
+ {
5395
+ "type": "text",
5396
+ "id": "-7qd6ZmMNssePA_ZVXOds"
5397
+ },
5398
+ {
5399
+ "id": "eXqdTotBC05JGtmSBdiba",
5400
+ "type": "arrow"
5401
+ },
5402
+ {
5403
+ "id": "RG4hRN73PDSPg9TvjEABI",
5404
+ "type": "arrow"
5405
+ }
5406
+ ],
5407
+ "updated": 1741577112902,
5408
+ "link": null,
5409
+ "locked": false
5410
+ },
5411
+ {
5412
+ "id": "zkIptPJSdTc7fyDulV4Nc",
5413
+ "type": "rectangle",
5414
+ "x": 7406.235244066099,
5415
+ "y": 532.5354253333344,
5416
+ "width": 236.28111124423486,
5417
+ "height": 44,
5418
+ "angle": 0,
5419
+ "strokeColor": "#1e1e1e",
5420
+ "backgroundColor": "transparent",
5421
+ "fillStyle": "solid",
5422
+ "strokeWidth": 2,
5423
+ "strokeStyle": "solid",
5424
+ "roughness": 1,
5425
+ "opacity": 100,
5426
+ "groupIds": [],
5427
+ "frameId": null,
5428
+ "index": "b1n",
5429
+ "roundness": null,
5430
+ "seed": 1997478153,
5431
+ "version": 154,
5432
+ "versionNonce": 717019239,
5433
+ "isDeleted": false,
5434
+ "boundElements": [
5435
+ {
5436
+ "type": "text",
5437
+ "id": "yhuAJFICJoRjDFmjFzM6C"
5438
+ },
5439
+ {
5440
+ "id": "F704_xUi4glbWWo1UMSZD",
5441
+ "type": "arrow"
5442
+ }
5443
+ ],
5444
+ "updated": 1741577022253,
5445
+ "link": null,
5446
+ "locked": false
5447
+ },
5448
+ {
5449
+ "id": "hktnzSdCZsg2XG9eDy1n-",
5450
+ "type": "arrow",
5451
+ "x": 6888.2769562202075,
5452
+ "y": 197.07411150064814,
5453
+ "width": 0.00048490204972040374,
5454
+ "height": 85.77724415899723,
5455
+ "angle": 0,
5456
+ "strokeColor": "#1e1e1e",
5457
+ "backgroundColor": "transparent",
5458
+ "fillStyle": "solid",
5459
+ "strokeWidth": 2,
5460
+ "strokeStyle": "solid",
5461
+ "roughness": 1,
5462
+ "opacity": 100,
5463
+ "groupIds": [
5464
+ "wjqPa5lw_KjJSGahompw4"
5465
+ ],
5466
+ "frameId": null,
5467
+ "index": "b1r",
5468
+ "roundness": {
5469
+ "type": 2
5470
+ },
5471
+ "seed": 2095697033,
5472
+ "version": 259,
5473
+ "versionNonce": 1243141799,
5474
+ "isDeleted": false,
5475
+ "boundElements": [],
5476
+ "updated": 1741576957690,
5477
+ "link": null,
5478
+ "locked": false,
5479
+ "points": [
5480
+ [
5481
+ 0,
5482
+ 0
5483
+ ],
5484
+ [
5485
+ 0,
5486
+ 46.015302918856854
5487
+ ],
5488
+ [
5489
+ -0.00048490204972040374,
5490
+ 85.77724415899723
5491
+ ]
5492
+ ],
5493
+ "lastCommittedPoint": null,
5494
+ "startBinding": null,
5495
+ "endBinding": {
5496
+ "elementId": "O4ynYwner8EHSv-cUHyx0",
5497
+ "focus": -0.0000018829436698626987,
5498
+ "gap": 6.843301459727456
5499
+ },
5500
+ "startArrowhead": null,
5501
+ "endArrowhead": "arrow",
5502
+ "elbowed": false
5503
+ },
5504
+ {
5505
+ "id": "eXqdTotBC05JGtmSBdiba",
5506
+ "type": "arrow",
5507
+ "x": 6702.601669303934,
5508
+ "y": 487.3244837580529,
5509
+ "width": 9.094947017729282e-13,
5510
+ "height": 85.77724415899729,
5511
+ "angle": 0,
5512
+ "strokeColor": "#1e1e1e",
5513
+ "backgroundColor": "transparent",
5514
+ "fillStyle": "solid",
5515
+ "strokeWidth": 2,
5516
+ "strokeStyle": "solid",
5517
+ "roughness": 1,
5518
+ "opacity": 100,
5519
+ "groupIds": [
5520
+ "wjqPa5lw_KjJSGahompw4"
5521
+ ],
5522
+ "frameId": null,
5523
+ "index": "b1x",
5524
+ "roundness": {
5525
+ "type": 2
5526
+ },
5527
+ "seed": 2118324681,
5528
+ "version": 261,
5529
+ "versionNonce": 1203561479,
5530
+ "isDeleted": false,
5531
+ "boundElements": [
5532
+ {
5533
+ "type": "text",
5534
+ "id": "urJkseY3HJY69Oariem4m"
5535
+ }
5536
+ ],
5537
+ "updated": 1741576957690,
5538
+ "link": null,
5539
+ "locked": false,
5540
+ "points": [
5541
+ [
5542
+ 0,
5543
+ 0
5544
+ ],
5545
+ [
5546
+ 0,
5547
+ 46.015302918856854
5548
+ ],
5549
+ [
5550
+ -9.094947017729282e-13,
5551
+ 85.77724415899729
5552
+ ]
5553
+ ],
5554
+ "lastCommittedPoint": null,
5555
+ "startBinding": null,
5556
+ "endBinding": {
5557
+ "elementId": "W854nPSz0p4lut302JyEo",
5558
+ "focus": -0.0000033181252662927257,
5559
+ "gap": 6.8433014597274
5560
+ },
5561
+ "startArrowhead": null,
5562
+ "endArrowhead": "arrow",
5563
+ "elbowed": false
5564
+ },
5565
+ {
5566
+ "id": "zSH_0RVoAH6FqvlC-qOEe",
5567
+ "type": "text",
5568
+ "x": 6894.8592730095925,
5569
+ "y": 722.2366477728008,
5570
+ "width": 44.43995666503906,
5571
+ "height": 25,
5572
+ "angle": 0,
5573
+ "strokeColor": "#1e1e1e",
5574
+ "backgroundColor": "transparent",
5575
+ "fillStyle": "solid",
5576
+ "strokeWidth": 2,
5577
+ "strokeStyle": "solid",
5578
+ "roughness": 1,
5579
+ "opacity": 100,
5580
+ "groupIds": [],
5581
+ "frameId": null,
5582
+ "index": "b21",
5583
+ "roundness": null,
5584
+ "seed": 1023108425,
5585
+ "version": 141,
5586
+ "versionNonce": 1817672585,
5587
+ "isDeleted": false,
5588
+ "boundElements": [],
5589
+ "updated": 1741577040688,
5590
+ "link": null,
5591
+ "locked": false,
5592
+ "text": "User",
5593
+ "fontSize": 20,
5594
+ "fontFamily": 5,
5595
+ "textAlign": "center",
5596
+ "verticalAlign": "middle",
5597
+ "containerId": "hh-vJczGSvSCpkhjc-ZFd",
5598
+ "originalText": "User",
5599
+ "autoResize": true,
5600
+ "lineHeight": 1.25
5601
+ },
5602
+ {
5603
+ "id": "YN7Z_MhrnVfus-HHFPTk1",
5604
+ "type": "text",
5605
+ "x": 6833.114194660153,
5606
+ "y": -66.0390308302513,
5607
+ "width": 212.7344970703125,
5608
+ "height": 29.496989050549267,
5609
+ "angle": 0,
5610
+ "strokeColor": "#1e1e1e",
5611
+ "backgroundColor": "transparent",
5612
+ "fillStyle": "solid",
5613
+ "strokeWidth": 2,
5614
+ "strokeStyle": "solid",
5615
+ "roughness": 1,
5616
+ "opacity": 100,
5617
+ "groupIds": [
5618
+ "wjqPa5lw_KjJSGahompw4"
5619
+ ],
5620
+ "frameId": null,
5621
+ "index": "b22",
5622
+ "roundness": null,
5623
+ "seed": 1288819753,
5624
+ "version": 112,
5625
+ "versionNonce": 441046665,
5626
+ "isDeleted": false,
5627
+ "boundElements": [],
5628
+ "updated": 1741576957668,
5629
+ "link": null,
5630
+ "locked": false,
5631
+ "text": "AWS API Gateway",
5632
+ "fontSize": 23.597591240439414,
5633
+ "fontFamily": 5,
5634
+ "textAlign": "center",
5635
+ "verticalAlign": "middle",
5636
+ "containerId": "5rQwEFQ0OLt7lCCh6ROrl",
5637
+ "originalText": "AWS API Gateway",
5638
+ "autoResize": true,
5639
+ "lineHeight": 1.25
5640
+ },
5641
+ {
5642
+ "id": "A45yyUfo1lPMEY8TmwNAv",
5643
+ "type": "text",
5644
+ "x": 6679.344878334033,
5645
+ "y": 44.869647999813935,
5646
+ "width": 141.58709716796875,
5647
+ "height": 29.496989050549267,
5648
+ "angle": 0,
5649
+ "strokeColor": "#1e1e1e",
5650
+ "backgroundColor": "transparent",
5651
+ "fillStyle": "solid",
5652
+ "strokeWidth": 2,
5653
+ "strokeStyle": "solid",
5654
+ "roughness": 1,
5655
+ "opacity": 100,
5656
+ "groupIds": [
5657
+ "wjqPa5lw_KjJSGahompw4"
5658
+ ],
5659
+ "frameId": null,
5660
+ "index": "b23",
5661
+ "roundness": null,
5662
+ "seed": 968320777,
5663
+ "version": 116,
5664
+ "versionNonce": 471452169,
5665
+ "isDeleted": false,
5666
+ "boundElements": [],
5667
+ "updated": 1741576957669,
5668
+ "link": null,
5669
+ "locked": false,
5670
+ "text": "AWS Lambda",
5671
+ "fontSize": 23.597591240439414,
5672
+ "fontFamily": 5,
5673
+ "textAlign": "center",
5674
+ "verticalAlign": "middle",
5675
+ "containerId": "lcNOx1PJ1LukZ3eN5WcGs",
5676
+ "originalText": "AWS Lambda",
5677
+ "autoResize": true,
5678
+ "lineHeight": 1.25
5679
+ },
5680
+ {
5681
+ "id": "fXbbtx_NcVZBC_fsO1Uff",
5682
+ "type": "text",
5683
+ "x": 6752.7992341022855,
5684
+ "y": 155.77832682987918,
5685
+ "width": 270.9545593261719,
5686
+ "height": 29.496989050549267,
5687
+ "angle": 0,
5688
+ "strokeColor": "#1e1e1e",
5689
+ "backgroundColor": "transparent",
5690
+ "fillStyle": "solid",
5691
+ "strokeWidth": 2,
5692
+ "strokeStyle": "solid",
5693
+ "roughness": 1,
5694
+ "opacity": 100,
5695
+ "groupIds": [
5696
+ "wjqPa5lw_KjJSGahompw4"
5697
+ ],
5698
+ "frameId": null,
5699
+ "index": "b24",
5700
+ "roundness": null,
5701
+ "seed": 1579654633,
5702
+ "version": 120,
5703
+ "versionNonce": 2083349609,
5704
+ "isDeleted": false,
5705
+ "boundElements": [],
5706
+ "updated": 1741576957669,
5707
+ "link": null,
5708
+ "locked": false,
5709
+ "text": "AWS SQS (Task Queue)",
5710
+ "fontSize": 23.597591240439414,
5711
+ "fontFamily": 5,
5712
+ "textAlign": "center",
5713
+ "verticalAlign": "middle",
5714
+ "containerId": "YPquWrf902Bx_VzPe5ZKo",
5715
+ "originalText": "AWS SQS (Task Queue)",
5716
+ "autoResize": true,
5717
+ "lineHeight": 1.25
5718
+ },
5719
+ {
5720
+ "id": "V3UN0NpFvZDTX16OkuBNR",
5721
+ "type": "text",
5722
+ "x": 6684.789361665762,
5723
+ "y": 300.90351295858153,
5724
+ "width": 406.97430419921875,
5725
+ "height": 29.496989050549267,
5726
+ "angle": 0,
5727
+ "strokeColor": "#1e1e1e",
5728
+ "backgroundColor": "transparent",
5729
+ "fillStyle": "solid",
5730
+ "strokeWidth": 2,
5731
+ "strokeStyle": "solid",
5732
+ "roughness": 1,
5733
+ "opacity": 100,
5734
+ "groupIds": [
5735
+ "wjqPa5lw_KjJSGahompw4"
5736
+ ],
5737
+ "frameId": null,
5738
+ "index": "b25",
5739
+ "roundness": null,
5740
+ "seed": 959314121,
5741
+ "version": 112,
5742
+ "versionNonce": 1184395529,
5743
+ "isDeleted": false,
5744
+ "boundElements": [],
5745
+ "updated": 1741576957669,
5746
+ "link": null,
5747
+ "locked": false,
5748
+ "text": "Celery Workers (Distributed Tasks)",
5749
+ "fontSize": 23.597591240439414,
5750
+ "fontFamily": 5,
5751
+ "textAlign": "center",
5752
+ "verticalAlign": "middle",
5753
+ "containerId": "O4ynYwner8EHSv-cUHyx0",
5754
+ "originalText": "Celery Workers (Distributed Tasks)",
5755
+ "autoResize": true,
5756
+ "lineHeight": 1.25
5757
+ },
5758
+ {
5759
+ "id": "lKO9DytZCWjoMyMUO9WnK",
5760
+ "type": "text",
5761
+ "x": 6252.558107535698,
5762
+ "y": 446.02869908728394,
5763
+ "width": 181.6428985595703,
5764
+ "height": 29.496989050549267,
5765
+ "angle": 0,
5766
+ "strokeColor": "#1e1e1e",
5767
+ "backgroundColor": "transparent",
5768
+ "fillStyle": "solid",
5769
+ "strokeWidth": 2,
5770
+ "strokeStyle": "solid",
5771
+ "roughness": 1,
5772
+ "opacity": 100,
5773
+ "groupIds": [
5774
+ "wjqPa5lw_KjJSGahompw4"
5775
+ ],
5776
+ "frameId": null,
5777
+ "index": "b26",
5778
+ "roundness": null,
5779
+ "seed": 1550186409,
5780
+ "version": 112,
5781
+ "versionNonce": 1604561769,
5782
+ "isDeleted": false,
5783
+ "boundElements": [],
5784
+ "updated": 1741576957670,
5785
+ "link": null,
5786
+ "locked": false,
5787
+ "text": "AWS DynamoDB",
5788
+ "fontSize": 23.597591240439414,
5789
+ "fontFamily": 5,
5790
+ "textAlign": "center",
5791
+ "verticalAlign": "middle",
5792
+ "containerId": "d-Obq1jyhqw7abPHd6_i5",
5793
+ "originalText": "AWS DynamoDB",
5794
+ "autoResize": true,
5795
+ "lineHeight": 1.25
5796
+ },
5797
+ {
5798
+ "id": "ZIBlXFowG3rpzs7ZAmiZZ",
5799
+ "type": "text",
5800
+ "x": 6574.75627638259,
5801
+ "y": 446.02869908728394,
5802
+ "width": 255.6918182373047,
5803
+ "height": 29.496989050549267,
5804
+ "angle": 0,
5805
+ "strokeColor": "#1e1e1e",
5806
+ "backgroundColor": "transparent",
5807
+ "fillStyle": "solid",
5808
+ "strokeWidth": 2,
5809
+ "strokeStyle": "solid",
5810
+ "roughness": 1,
5811
+ "opacity": 100,
5812
+ "groupIds": [
5813
+ "wjqPa5lw_KjJSGahompw4"
5814
+ ],
5815
+ "frameId": null,
5816
+ "index": "b27",
5817
+ "roundness": null,
5818
+ "seed": 435886729,
5819
+ "version": 120,
5820
+ "versionNonce": 1559375593,
5821
+ "isDeleted": false,
5822
+ "boundElements": [],
5823
+ "updated": 1741576957670,
5824
+ "link": null,
5825
+ "locked": false,
5826
+ "text": "AWS S3 (File Storage)",
5827
+ "fontSize": 23.597591240439414,
5828
+ "fontFamily": 5,
5829
+ "textAlign": "center",
5830
+ "verticalAlign": "middle",
5831
+ "containerId": "SLQjJ54OvcwHKgE0saBYW",
5832
+ "originalText": "AWS S3 (File Storage)",
5833
+ "autoResize": true,
5834
+ "lineHeight": 1.25
5835
+ },
5836
+ {
5837
+ "id": "avhHlotKdqzZ5gQytgcwQ",
5838
+ "type": "text",
5839
+ "x": 6933.366338855672,
5840
+ "y": 446.02869908728394,
5841
+ "width": 281.16900634765625,
5842
+ "height": 29.496989050549267,
5843
+ "angle": 0,
5844
+ "strokeColor": "#1e1e1e",
5845
+ "backgroundColor": "transparent",
5846
+ "fillStyle": "solid",
5847
+ "strokeWidth": 2,
5848
+ "strokeStyle": "solid",
5849
+ "roughness": 1,
5850
+ "opacity": 100,
5851
+ "groupIds": [
5852
+ "wjqPa5lw_KjJSGahompw4"
5853
+ ],
5854
+ "frameId": null,
5855
+ "index": "b28",
5856
+ "roundness": null,
5857
+ "seed": 1923670377,
5858
+ "version": 120,
5859
+ "versionNonce": 748899209,
5860
+ "isDeleted": false,
5861
+ "boundElements": [],
5862
+ "updated": 1741576957670,
5863
+ "link": null,
5864
+ "locked": false,
5865
+ "text": "AWS SNS (Notifications)",
5866
+ "fontSize": 23.597591240439414,
5867
+ "fontFamily": 5,
5868
+ "textAlign": "center",
5869
+ "verticalAlign": "middle",
5870
+ "containerId": "SmhmRdO5OootjWvVdhZVC",
5871
+ "originalText": "AWS SNS (Notifications)",
5872
+ "autoResize": true,
5873
+ "lineHeight": 1.25
5874
+ },
5875
+ {
5876
+ "id": "GHHkKHEJHxyvjEsluWanq",
5877
+ "type": "text",
5878
+ "x": 7430.640893404734,
5879
+ "y": 446.02869908728394,
5880
+ "width": 189.3568115234375,
5881
+ "height": 29.496989050549267,
5882
+ "angle": 0,
5883
+ "strokeColor": "#1e1e1e",
5884
+ "backgroundColor": "transparent",
5885
+ "fillStyle": "solid",
5886
+ "strokeWidth": 2,
5887
+ "strokeStyle": "solid",
5888
+ "roughness": 1,
5889
+ "opacity": 100,
5890
+ "groupIds": [
5891
+ "wjqPa5lw_KjJSGahompw4"
5892
+ ],
5893
+ "frameId": null,
5894
+ "index": "b29",
5895
+ "roundness": null,
5896
+ "seed": 1136315465,
5897
+ "version": 112,
5898
+ "versionNonce": 150350889,
5899
+ "isDeleted": false,
5900
+ "boundElements": [],
5901
+ "updated": 1741576957670,
5902
+ "link": null,
5903
+ "locked": false,
5904
+ "text": "AWS CloudWatch",
5905
+ "fontSize": 23.597591240439414,
5906
+ "fontFamily": 5,
5907
+ "textAlign": "center",
5908
+ "verticalAlign": "middle",
5909
+ "containerId": "-AKHuCF8e7h2_yxWV-Lgt",
5910
+ "originalText": "AWS CloudWatch",
5911
+ "autoResize": true,
5912
+ "lineHeight": 1.25
5913
+ },
5914
+ {
5915
+ "id": "-7qd6ZmMNssePA_ZVXOds",
5916
+ "type": "text",
5917
+ "x": 6571.94904829421,
5918
+ "y": 591.1538852159863,
5919
+ "width": 261.3062744140625,
5920
+ "height": 29.496989050549267,
5921
+ "angle": 0,
5922
+ "strokeColor": "#1e1e1e",
5923
+ "backgroundColor": "transparent",
5924
+ "fillStyle": "solid",
5925
+ "strokeWidth": 2,
5926
+ "strokeStyle": "solid",
5927
+ "roughness": 1,
5928
+ "opacity": 100,
5929
+ "groupIds": [
5930
+ "wjqPa5lw_KjJSGahompw4"
5931
+ ],
5932
+ "frameId": null,
5933
+ "index": "b2A",
5934
+ "roundness": null,
5935
+ "seed": 890002217,
5936
+ "version": 112,
5937
+ "versionNonce": 396251049,
5938
+ "isDeleted": false,
5939
+ "boundElements": [],
5940
+ "updated": 1741576957670,
5941
+ "link": null,
5942
+ "locked": false,
5943
+ "text": "AWS CloudFront (CDN)",
5944
+ "fontSize": 23.597591240439414,
5945
+ "fontFamily": 5,
5946
+ "textAlign": "center",
5947
+ "verticalAlign": "middle",
5948
+ "containerId": "W854nPSz0p4lut302JyEo",
5949
+ "originalText": "AWS CloudFront (CDN)",
5950
+ "autoResize": true,
5951
+ "lineHeight": 1.25
5952
+ },
5953
+ {
5954
+ "id": "yhuAJFICJoRjDFmjFzM6C",
5955
+ "type": "text",
5956
+ "x": 7432.925856145735,
5957
+ "y": 542.0354253333344,
5958
+ "width": 182.89988708496094,
5959
+ "height": 25,
5960
+ "angle": 0,
5961
+ "strokeColor": "#1e1e1e",
5962
+ "backgroundColor": "transparent",
5963
+ "fillStyle": "solid",
5964
+ "strokeWidth": 2,
5965
+ "strokeStyle": "solid",
5966
+ "roughness": 1,
5967
+ "opacity": 100,
5968
+ "groupIds": [],
5969
+ "frameId": null,
5970
+ "index": "b2B",
5971
+ "roundness": null,
5972
+ "seed": 1728646665,
5973
+ "version": 148,
5974
+ "versionNonce": 1559308967,
5975
+ "isDeleted": false,
5976
+ "boundElements": [],
5977
+ "updated": 1741577022253,
5978
+ "link": null,
5979
+ "locked": false,
5980
+ "text": "DevOps Monitoring",
5981
+ "fontSize": 20,
5982
+ "fontFamily": 5,
5983
+ "textAlign": "center",
5984
+ "verticalAlign": "middle",
5985
+ "containerId": "zkIptPJSdTc7fyDulV4Nc",
5986
+ "originalText": "DevOps Monitoring",
5987
+ "autoResize": true,
5988
+ "lineHeight": 1.25
5989
+ },
5990
+ {
5991
+ "id": "urJkseY3HJY69Oariem4m",
5992
+ "type": "text",
5993
+ "x": 6614.139244072,
5994
+ "y": 518.5912921516351,
5995
+ "width": 176.9248504638672,
5996
+ "height": 29.496989050549267,
5997
+ "angle": 0,
5998
+ "strokeColor": "#1e1e1e",
5999
+ "backgroundColor": "transparent",
6000
+ "fillStyle": "solid",
6001
+ "strokeWidth": 2,
6002
+ "strokeStyle": "solid",
6003
+ "roughness": 1,
6004
+ "opacity": 100,
6005
+ "groupIds": [
6006
+ "wjqPa5lw_KjJSGahompw4"
6007
+ ],
6008
+ "frameId": null,
6009
+ "index": "b2I",
6010
+ "roundness": null,
6011
+ "seed": 1695539753,
6012
+ "version": 103,
6013
+ "versionNonce": 1733728649,
6014
+ "isDeleted": false,
6015
+ "boundElements": [],
6016
+ "updated": 1741576957670,
6017
+ "link": null,
6018
+ "locked": false,
6019
+ "text": "Serves Content",
6020
+ "fontSize": 23.597591240439414,
6021
+ "fontFamily": 5,
6022
+ "textAlign": "center",
6023
+ "verticalAlign": "middle",
6024
+ "containerId": "eXqdTotBC05JGtmSBdiba",
6025
+ "originalText": "Serves Content",
6026
+ "autoResize": true,
6027
+ "lineHeight": 1.25
6028
+ },
6029
+ {
6030
+ "id": "QwQpbmMZYUJCdK5IFIkvr",
6031
+ "type": "text",
6032
+ "x": 6544.071555396827,
6033
+ "y": -344.4583329999999,
6034
+ "width": 849.491943359375,
6035
+ "height": 105,
6036
+ "angle": 0,
6037
+ "strokeColor": "#1e1e1e",
6038
+ "backgroundColor": "transparent",
6039
+ "fillStyle": "solid",
6040
+ "strokeWidth": 2,
6041
+ "strokeStyle": "solid",
6042
+ "roughness": 1,
6043
+ "opacity": 100,
6044
+ "groupIds": [],
6045
+ "frameId": null,
6046
+ "index": "b2K",
6047
+ "roundness": null,
6048
+ "seed": 925609498,
6049
+ "version": 680,
6050
+ "versionNonce": 1532562041,
6051
+ "isDeleted": false,
6052
+ "boundElements": [],
6053
+ "updated": 1741577153339,
6054
+ "link": null,
6055
+ "locked": false,
6056
+ "text": "Cloud Infrastructure",
6057
+ "fontSize": 84,
6058
+ "fontFamily": 5,
6059
+ "textAlign": "left",
6060
+ "verticalAlign": "top",
6061
+ "containerId": null,
6062
+ "originalText": "Cloud Infrastructure",
6063
+ "autoResize": true,
6064
+ "lineHeight": 1.25
6065
+ },
6066
+ {
6067
+ "id": "Gr_IecqseQ5STfEBPzVML",
6068
+ "type": "arrow",
6069
+ "x": 6939.381443195309,
6070
+ "y": -20.33318594049333,
6071
+ "width": 189.343016277292,
6072
+ "height": 48.99397810109854,
6073
+ "angle": 0,
6074
+ "strokeColor": "#1e1e1e",
6075
+ "backgroundColor": "transparent",
6076
+ "fillStyle": "solid",
6077
+ "strokeWidth": 2,
6078
+ "strokeStyle": "solid",
6079
+ "roughness": 1,
6080
+ "opacity": 100,
6081
+ "groupIds": [],
6082
+ "frameId": null,
6083
+ "index": "b2L",
6084
+ "roundness": null,
6085
+ "seed": 1516912969,
6086
+ "version": 12,
6087
+ "versionNonce": 742760681,
6088
+ "isDeleted": false,
6089
+ "boundElements": [],
6090
+ "updated": 1741576957669,
6091
+ "link": null,
6092
+ "locked": false,
6093
+ "points": [
6094
+ [
6095
+ 0,
6096
+ 0
6097
+ ],
6098
+ [
6099
+ 0,
6100
+ 24.496989050549274
6101
+ ],
6102
+ [
6103
+ -189.343016277292,
6104
+ 24.496989050549274
6105
+ ],
6106
+ [
6107
+ -189.343016277292,
6108
+ 48.99397810109854
6109
+ ]
6110
+ ],
6111
+ "lastCommittedPoint": null,
6112
+ "startBinding": {
6113
+ "elementId": "5rQwEFQ0OLt7lCCh6ROrl",
6114
+ "fixedPoint": [
6115
+ 0.49959225118778955,
6116
+ 1.096311833253238
6117
+ ],
6118
+ "focus": 0,
6119
+ "gap": 0
6120
+ },
6121
+ "endBinding": {
6122
+ "elementId": "lcNOx1PJ1LukZ3eN5WcGs",
6123
+ "fixedPoint": [
6124
+ 0.4994565391795548,
6125
+ -0.0963118332532381
6126
+ ],
6127
+ "focus": 0,
6128
+ "gap": 0
6129
+ },
6130
+ "startArrowhead": null,
6131
+ "endArrowhead": "arrow",
6132
+ "elbowed": true,
6133
+ "fixedSegments": null,
6134
+ "startIsSpecial": null,
6135
+ "endIsSpecial": null
6136
+ },
6137
+ {
6138
+ "id": "jXazvlSAB2utF1woHHpxJ",
6139
+ "type": "arrow",
6140
+ "x": 6750.038426918017,
6141
+ "y": 90.5754928895719,
6142
+ "width": 138.13808684735432,
6143
+ "height": 48.99397810109855,
6144
+ "angle": 0,
6145
+ "strokeColor": "#1e1e1e",
6146
+ "backgroundColor": "transparent",
6147
+ "fillStyle": "solid",
6148
+ "strokeWidth": 2,
6149
+ "strokeStyle": "solid",
6150
+ "roughness": 1,
6151
+ "opacity": 100,
6152
+ "groupIds": [],
6153
+ "frameId": null,
6154
+ "index": "b2M",
6155
+ "roundness": null,
6156
+ "seed": 535680231,
6157
+ "version": 12,
6158
+ "versionNonce": 1337388873,
6159
+ "isDeleted": false,
6160
+ "boundElements": [],
6161
+ "updated": 1741576957669,
6162
+ "link": null,
6163
+ "locked": false,
6164
+ "points": [
6165
+ [
6166
+ 0,
6167
+ 0
6168
+ ],
6169
+ [
6170
+ 0,
6171
+ 24.496989050549274
6172
+ ],
6173
+ [
6174
+ 138.13808684735432,
6175
+ 24.496989050549274
6176
+ ],
6177
+ [
6178
+ 138.13808684735432,
6179
+ 48.99397810109855
6180
+ ]
6181
+ ],
6182
+ "lastCommittedPoint": null,
6183
+ "startBinding": {
6184
+ "elementId": "lcNOx1PJ1LukZ3eN5WcGs",
6185
+ "fixedPoint": [
6186
+ 0.4994565391795548,
6187
+ 1.096311833253238
6188
+ ],
6189
+ "focus": 0,
6190
+ "gap": 0
6191
+ },
6192
+ "endBinding": {
6193
+ "elementId": "YPquWrf902Bx_VzPe5ZKo",
6194
+ "fixedPoint": [
6195
+ 0.4996759300723609,
6196
+ -0.0963118332532381
6197
+ ],
6198
+ "focus": 0,
6199
+ "gap": 0
6200
+ },
6201
+ "startArrowhead": null,
6202
+ "endArrowhead": "arrow",
6203
+ "elbowed": true,
6204
+ "fixedSegments": null,
6205
+ "startIsSpecial": null,
6206
+ "endIsSpecial": null
6207
+ },
6208
+ {
6209
+ "id": "3raa-F9gTwZ7ZG2sCiVMU",
6210
+ "type": "arrow",
6211
+ "x": 6228.346569847856,
6212
+ "y": 460.6771936125586,
6213
+ "width": 459.7889046596829,
6214
+ "height": 401.15905108747006,
6215
+ "angle": 0,
6216
+ "strokeColor": "#1e1e1e",
6217
+ "backgroundColor": "transparent",
6218
+ "fillStyle": "solid",
6219
+ "strokeWidth": 2,
6220
+ "strokeStyle": "solid",
6221
+ "roughness": 1,
6222
+ "opacity": 100,
6223
+ "groupIds": [],
6224
+ "frameId": null,
6225
+ "index": "b2N",
6226
+ "roundness": null,
6227
+ "seed": 1355940775,
6228
+ "version": 13,
6229
+ "versionNonce": 449599049,
6230
+ "isDeleted": false,
6231
+ "boundElements": [
6232
+ {
6233
+ "type": "text",
6234
+ "id": "N-p9QoQ7IUIthn6hq46No"
6235
+ }
6236
+ ],
6237
+ "updated": 1741576957670,
6238
+ "link": null,
6239
+ "locked": false,
6240
+ "points": [
6241
+ [
6242
+ 0,
6243
+ 0
6244
+ ],
6245
+ [
6246
+ -35,
6247
+ 0
6248
+ ],
6249
+ [
6250
+ -35,
6251
+ -401.15905108747006
6252
+ ],
6253
+ [
6254
+ 424.7889046596829,
6255
+ -401.15905108747006
6256
+ ]
6257
+ ],
6258
+ "lastCommittedPoint": null,
6259
+ "startBinding": {
6260
+ "elementId": "d-Obq1jyhqw7abPHd6_i5",
6261
+ "fixedPoint": [
6262
+ -0.02272045928132014,
6263
+ 0.4980737633349352
6264
+ ],
6265
+ "focus": 0,
6266
+ "gap": 0
6267
+ },
6268
+ "endBinding": {
6269
+ "elementId": "lcNOx1PJ1LukZ3eN5WcGs",
6270
+ "fixedPoint": [
6271
+ -0.02717304102205375,
6272
+ 0.49807376333493464
6273
+ ],
6274
+ "focus": 0,
6275
+ "gap": 0
6276
+ },
6277
+ "startArrowhead": null,
6278
+ "endArrowhead": "arrow",
6279
+ "elbowed": true,
6280
+ "fixedSegments": null,
6281
+ "startIsSpecial": null,
6282
+ "endIsSpecial": null
6283
+ },
6284
+ {
6285
+ "id": "N-p9QoQ7IUIthn6hq46No",
6286
+ "type": "text",
6287
+ "x": 6106.646611046586,
6288
+ "y": 245.59254412216723,
6289
+ "width": 173.39991760253906,
6290
+ "height": 30,
6291
+ "angle": 0,
6292
+ "strokeColor": "#1e1e1e",
6293
+ "backgroundColor": "transparent",
6294
+ "fillStyle": "solid",
6295
+ "strokeWidth": 2,
6296
+ "strokeStyle": "solid",
6297
+ "roughness": 1,
6298
+ "opacity": 100,
6299
+ "groupIds": [],
6300
+ "frameId": null,
6301
+ "index": "b2O",
6302
+ "roundness": null,
6303
+ "seed": 121996329,
6304
+ "version": 51,
6305
+ "versionNonce": 1428605609,
6306
+ "isDeleted": false,
6307
+ "boundElements": [],
6308
+ "updated": 1741576898934,
6309
+ "link": null,
6310
+ "locked": false,
6311
+ "text": "Provides Data",
6312
+ "fontSize": 24,
6313
+ "fontFamily": 1,
6314
+ "textAlign": "center",
6315
+ "verticalAlign": "middle",
6316
+ "containerId": "3raa-F9gTwZ7ZG2sCiVMU",
6317
+ "originalText": "Provides Data",
6318
+ "autoResize": true,
6319
+ "lineHeight": 1.25
6320
+ },
6321
+ {
6322
+ "id": "fMbl9KWZiggWCvF5xaMRT",
6323
+ "type": "arrow",
6324
+ "x": 6648.296124741433,
6325
+ "y": 315.5520074838562,
6326
+ "width": 305.01656792595077,
6327
+ "height": 114.26783576421906,
6328
+ "angle": 0,
6329
+ "strokeColor": "#1e1e1e",
6330
+ "backgroundColor": "transparent",
6331
+ "fillStyle": "solid",
6332
+ "strokeWidth": 2,
6333
+ "strokeStyle": "solid",
6334
+ "roughness": 1,
6335
+ "opacity": 100,
6336
+ "groupIds": [],
6337
+ "frameId": null,
6338
+ "index": "b2P",
6339
+ "roundness": null,
6340
+ "seed": 1912476361,
6341
+ "version": 12,
6342
+ "versionNonce": 89957673,
6343
+ "isDeleted": false,
6344
+ "boundElements": [
6345
+ {
6346
+ "type": "text",
6347
+ "id": "TrZYRDh3Zbhh-Y8oME2XA"
6348
+ }
6349
+ ],
6350
+ "updated": 1741576957670,
6351
+ "link": null,
6352
+ "locked": false,
6353
+ "points": [
6354
+ [
6355
+ 0,
6356
+ 0
6357
+ ],
6358
+ [
6359
+ -305.01656792595077,
6360
+ 0
6361
+ ],
6362
+ [
6363
+ -305.01656792595077,
6364
+ 114.26783576421906
6365
+ ]
6366
+ ],
6367
+ "lastCommittedPoint": null,
6368
+ "startBinding": {
6369
+ "elementId": "O4ynYwner8EHSv-cUHyx0",
6370
+ "fixedPoint": [
6371
+ -0.010639185722623509,
6372
+ 0.4980737633349352
6373
+ ],
6374
+ "focus": 0,
6375
+ "gap": 0
6376
+ },
6377
+ "endBinding": {
6378
+ "elementId": "d-Obq1jyhqw7abPHd6_i5",
6379
+ "fixedPoint": [
6380
+ 0.4995455908143719,
6381
+ -0.0963118332532381
6382
+ ],
6383
+ "focus": 0,
6384
+ "gap": 0
6385
+ },
6386
+ "startArrowhead": null,
6387
+ "endArrowhead": "arrow",
6388
+ "elbowed": true,
6389
+ "fixedSegments": null,
6390
+ "startIsSpecial": null,
6391
+ "endIsSpecial": null
6392
+ },
6393
+ {
6394
+ "id": "TrZYRDh3Zbhh-Y8oME2XA",
6395
+ "type": "text",
6396
+ "x": 6242.311600272514,
6397
+ "y": 301.04688353719985,
6398
+ "width": 201.9359130859375,
6399
+ "height": 30,
6400
+ "angle": 0,
6401
+ "strokeColor": "#1e1e1e",
6402
+ "backgroundColor": "transparent",
6403
+ "fillStyle": "solid",
6404
+ "strokeWidth": 2,
6405
+ "strokeStyle": "solid",
6406
+ "roughness": 1,
6407
+ "opacity": 100,
6408
+ "groupIds": [],
6409
+ "frameId": null,
6410
+ "index": "b2Q",
6411
+ "roundness": null,
6412
+ "seed": 1768413353,
6413
+ "version": 21,
6414
+ "versionNonce": 876256521,
6415
+ "isDeleted": false,
6416
+ "boundElements": [],
6417
+ "updated": 1741576917613,
6418
+ "link": null,
6419
+ "locked": false,
6420
+ "text": "Processes Tasks",
6421
+ "fontSize": 24,
6422
+ "fontFamily": 1,
6423
+ "textAlign": "center",
6424
+ "verticalAlign": "middle",
6425
+ "containerId": "fMbl9KWZiggWCvF5xaMRT",
6426
+ "originalText": "Processes Tasks",
6427
+ "autoResize": true,
6428
+ "lineHeight": 1.25
6429
+ },
6430
+ {
6431
+ "id": "QTMS_iw6t6ynVYi-LOM0h",
6432
+ "type": "arrow",
6433
+ "x": 6763.232639511854,
6434
+ "y": 342.66514912265495,
6435
+ "width": 60.73045401061245,
6436
+ "height": 87.15469412542029,
6437
+ "angle": 0,
6438
+ "strokeColor": "#1e1e1e",
6439
+ "backgroundColor": "transparent",
6440
+ "fillStyle": "solid",
6441
+ "strokeWidth": 2,
6442
+ "strokeStyle": "solid",
6443
+ "roughness": 1,
6444
+ "opacity": 100,
6445
+ "groupIds": [],
6446
+ "frameId": null,
6447
+ "index": "b2R",
6448
+ "roundness": null,
6449
+ "seed": 735937097,
6450
+ "version": 15,
6451
+ "versionNonce": 41839049,
6452
+ "isDeleted": false,
6453
+ "boundElements": [
6454
+ {
6455
+ "type": "text",
6456
+ "id": "izQ2ocL7VnsI6ZPX9Eijt"
6457
+ }
6458
+ ],
6459
+ "updated": 1741576957670,
6460
+ "link": null,
6461
+ "locked": false,
6462
+ "points": [
6463
+ [
6464
+ 0,
6465
+ 0
6466
+ ],
6467
+ [
6468
+ 0,
6469
+ 45.549451425552434
6470
+ ],
6471
+ [
6472
+ -60.73045401061245,
6473
+ 45.549451425552434
6474
+ ],
6475
+ [
6476
+ -60.73045401061245,
6477
+ 87.15469412542029
6478
+ ]
6479
+ ],
6480
+ "lastCommittedPoint": null,
6481
+ "startBinding": {
6482
+ "elementId": "O4ynYwner8EHSv-cUHyx0",
6483
+ "fixedPoint": [
6484
+ 0.23392699966809058,
6485
+ 1.020337038632418
6486
+ ],
6487
+ "focus": 0,
6488
+ "gap": 0
6489
+ },
6490
+ "endBinding": {
6491
+ "elementId": "SLQjJ54OvcwHKgE0saBYW",
6492
+ "fixedPoint": [
6493
+ 0.4996645050439854,
6494
+ -0.0963118332532381
6495
+ ],
6496
+ "focus": 0,
6497
+ "gap": 0
6498
+ },
6499
+ "startArrowhead": null,
6500
+ "endArrowhead": "arrow",
6501
+ "elbowed": true,
6502
+ "fixedSegments": null,
6503
+ "startIsSpecial": null,
6504
+ "endIsSpecial": null
6505
+ },
6506
+ {
6507
+ "id": "izQ2ocL7VnsI6ZPX9Eijt",
6508
+ "type": "text",
6509
+ "x": 6700.622463165728,
6510
+ "y": 382.45947660155105,
6511
+ "width": 64.48989868164062,
6512
+ "height": 12.5,
6513
+ "angle": 0,
6514
+ "strokeColor": "#1e1e1e",
6515
+ "backgroundColor": "transparent",
6516
+ "fillStyle": "solid",
6517
+ "strokeWidth": 2,
6518
+ "strokeStyle": "solid",
6519
+ "roughness": 1,
6520
+ "opacity": 100,
6521
+ "groupIds": [],
6522
+ "frameId": null,
6523
+ "index": "b2S",
6524
+ "roundness": null,
6525
+ "seed": 1834674953,
6526
+ "version": 47,
6527
+ "versionNonce": 128581577,
6528
+ "isDeleted": false,
6529
+ "boundElements": [],
6530
+ "updated": 1741576945070,
6531
+ "link": null,
6532
+ "locked": false,
6533
+ "text": "Uploads Files",
6534
+ "fontSize": 10,
6535
+ "fontFamily": 1,
6536
+ "textAlign": "center",
6537
+ "verticalAlign": "middle",
6538
+ "containerId": "QTMS_iw6t6ynVYi-LOM0h",
6539
+ "originalText": "Uploads Files",
6540
+ "autoResize": true,
6541
+ "lineHeight": 1.25
6542
+ },
6543
+ {
6544
+ "id": "FCEWIaj7DywBOG0ukJrRU",
6545
+ "type": "arrow",
6546
+ "x": 6967.973318464036,
6547
+ "y": 343.24522078594714,
6548
+ "width": 105.87752356546389,
6549
+ "height": 86.5746224621281,
6550
+ "angle": 0,
6551
+ "strokeColor": "#1e1e1e",
6552
+ "backgroundColor": "transparent",
6553
+ "fillStyle": "solid",
6554
+ "strokeWidth": 2,
6555
+ "strokeStyle": "solid",
6556
+ "roughness": 1,
6557
+ "opacity": 100,
6558
+ "groupIds": [],
6559
+ "frameId": null,
6560
+ "index": "b2T",
6561
+ "roundness": null,
6562
+ "seed": 582166663,
6563
+ "version": 16,
6564
+ "versionNonce": 408267401,
6565
+ "isDeleted": false,
6566
+ "boundElements": [
6567
+ {
6568
+ "type": "text",
6569
+ "id": "iJzD-V1Ye1tztyDSMKCCB"
6570
+ }
6571
+ ],
6572
+ "updated": 1741576972987,
6573
+ "link": null,
6574
+ "locked": false,
6575
+ "points": [
6576
+ [
6577
+ 0,
6578
+ 0
6579
+ ],
6580
+ [
6581
+ 0,
6582
+ 44.969379762260246
6583
+ ],
6584
+ [
6585
+ 105.87752356546389,
6586
+ 44.969379762260246
6587
+ ],
6588
+ [
6589
+ 105.87752356546389,
6590
+ 86.5746224621281
6591
+ ]
6592
+ ],
6593
+ "lastCommittedPoint": null,
6594
+ "startBinding": {
6595
+ "elementId": "O4ynYwner8EHSv-cUHyx0",
6596
+ "fixedPoint": [
6597
+ 0.6695818213377501,
6598
+ 1.031510591694403
6599
+ ],
6600
+ "focus": 0,
6601
+ "gap": 0
6602
+ },
6603
+ "endBinding": {
6604
+ "elementId": "SmhmRdO5OootjWvVdhZVC",
6605
+ "fixedPoint": [
6606
+ 0.4996938546986774,
6607
+ -0.0963118332532381
6608
+ ],
6609
+ "focus": 0,
6610
+ "gap": 0
6611
+ },
6612
+ "startArrowhead": null,
6613
+ "endArrowhead": "arrow",
6614
+ "elbowed": true,
6615
+ "fixedSegments": null,
6616
+ "startIsSpecial": null,
6617
+ "endIsSpecial": null
6618
+ },
6619
+ {
6620
+ "id": "iJzD-V1Ye1tztyDSMKCCB",
6621
+ "type": "text",
6622
+ "x": 6951.498681504092,
6623
+ "y": 380.0896005482074,
6624
+ "width": 138.82679748535156,
6625
+ "height": 16.25,
6626
+ "angle": 0,
6627
+ "strokeColor": "#1e1e1e",
6628
+ "backgroundColor": "transparent",
6629
+ "fillStyle": "solid",
6630
+ "strokeWidth": 2,
6631
+ "strokeStyle": "solid",
6632
+ "roughness": 1,
6633
+ "opacity": 100,
6634
+ "groupIds": [],
6635
+ "frameId": null,
6636
+ "index": "b2U",
6637
+ "roundness": null,
6638
+ "seed": 921728519,
6639
+ "version": 34,
6640
+ "versionNonce": 1066665031,
6641
+ "isDeleted": false,
6642
+ "boundElements": [],
6643
+ "updated": 1741576971298,
6644
+ "link": null,
6645
+ "locked": false,
6646
+ "text": "Triggers Notifications",
6647
+ "fontSize": 13,
6648
+ "fontFamily": 1,
6649
+ "textAlign": "center",
6650
+ "verticalAlign": "middle",
6651
+ "containerId": "FCEWIaj7DywBOG0ukJrRU",
6652
+ "originalText": "Triggers Notifications",
6653
+ "autoResize": true,
6654
+ "lineHeight": 1.25
6655
+ },
6656
+ {
6657
+ "id": "vZM--SBjM4zyowOgs3hDv",
6658
+ "type": "arrow",
6659
+ "x": 7128.25690278931,
6660
+ "y": 315.5520074838562,
6661
+ "width": 396.9623963771428,
6662
+ "height": 114.26783576421906,
6663
+ "angle": 0,
6664
+ "strokeColor": "#1e1e1e",
6665
+ "backgroundColor": "transparent",
6666
+ "fillStyle": "solid",
6667
+ "strokeWidth": 2,
6668
+ "strokeStyle": "solid",
6669
+ "roughness": 1,
6670
+ "opacity": 100,
6671
+ "groupIds": [],
6672
+ "frameId": null,
6673
+ "index": "b2V",
6674
+ "roundness": null,
6675
+ "seed": 1147991273,
6676
+ "version": 19,
6677
+ "versionNonce": 675995913,
6678
+ "isDeleted": false,
6679
+ "boundElements": [
6680
+ {
6681
+ "type": "text",
6682
+ "id": "10MDw0etYuPh4CpgAeTg0"
6683
+ }
6684
+ ],
6685
+ "updated": 1741577001129,
6686
+ "link": null,
6687
+ "locked": false,
6688
+ "points": [
6689
+ [
6690
+ 0,
6691
+ 0
6692
+ ],
6693
+ [
6694
+ 396.9623963771428,
6695
+ 0
6696
+ ],
6697
+ [
6698
+ 396.9623963771428,
6699
+ 114.26783576421906
6700
+ ]
6701
+ ],
6702
+ "lastCommittedPoint": null,
6703
+ "startBinding": {
6704
+ "elementId": "O4ynYwner8EHSv-cUHyx0",
6705
+ "fixedPoint": [
6706
+ 1.0106391857226238,
6707
+ 0.4980737633349352
6708
+ ],
6709
+ "focus": 0,
6710
+ "gap": 0
6711
+ },
6712
+ "endBinding": {
6713
+ "elementId": "-AKHuCF8e7h2_yxWV-Lgt",
6714
+ "fixedPoint": [
6715
+ 0.49957777827906663,
6716
+ -0.0963118332532381
6717
+ ],
6718
+ "focus": 0,
6719
+ "gap": 0
6720
+ },
6721
+ "startArrowhead": null,
6722
+ "endArrowhead": "arrow",
6723
+ "elbowed": true,
6724
+ "fixedSegments": null,
6725
+ "startIsSpecial": null,
6726
+ "endIsSpecial": null
6727
+ },
6728
+ {
6729
+ "id": "10MDw0etYuPh4CpgAeTg0",
6730
+ "type": "text",
6731
+ "x": 7451.974365084421,
6732
+ "y": 303.6770074838562,
6733
+ "width": 146.4898681640625,
6734
+ "height": 23.75,
6735
+ "angle": 0,
6736
+ "strokeColor": "#1e1e1e",
6737
+ "backgroundColor": "transparent",
6738
+ "fillStyle": "solid",
6739
+ "strokeWidth": 2,
6740
+ "strokeStyle": "solid",
6741
+ "roughness": 1,
6742
+ "opacity": 100,
6743
+ "groupIds": [],
6744
+ "frameId": null,
6745
+ "index": "b2W",
6746
+ "roundness": null,
6747
+ "seed": 2022324935,
6748
+ "version": 27,
6749
+ "versionNonce": 1790563783,
6750
+ "isDeleted": false,
6751
+ "boundElements": [],
6752
+ "updated": 1741576999044,
6753
+ "link": null,
6754
+ "locked": false,
6755
+ "text": "Logs Metadata",
6756
+ "fontSize": 19,
6757
+ "fontFamily": 1,
6758
+ "textAlign": "center",
6759
+ "verticalAlign": "middle",
6760
+ "containerId": "vZM--SBjM4zyowOgs3hDv",
6761
+ "originalText": "Logs Metadata",
6762
+ "autoResize": true,
6763
+ "lineHeight": 1.25
6764
+ },
6765
+ {
6766
+ "id": "F704_xUi4glbWWo1UMSZD",
6767
+ "type": "arrow",
6768
+ "x": 7401.898105937576,
6769
+ "y": 460.6771936125586,
6770
+ "width": 35.66286187147671,
6771
+ "height": 93.75823172077577,
6772
+ "angle": 0,
6773
+ "strokeColor": "#1e1e1e",
6774
+ "backgroundColor": "transparent",
6775
+ "fillStyle": "solid",
6776
+ "strokeWidth": 2,
6777
+ "strokeStyle": "solid",
6778
+ "roughness": 1,
6779
+ "opacity": 100,
6780
+ "groupIds": [],
6781
+ "frameId": null,
6782
+ "index": "b2X",
6783
+ "roundness": null,
6784
+ "seed": 495535463,
6785
+ "version": 157,
6786
+ "versionNonce": 772975431,
6787
+ "isDeleted": false,
6788
+ "boundElements": [],
6789
+ "updated": 1741577022658,
6790
+ "link": null,
6791
+ "locked": false,
6792
+ "points": [
6793
+ [
6794
+ 0,
6795
+ 0
6796
+ ],
6797
+ [
6798
+ -35,
6799
+ 0
6800
+ ],
6801
+ [
6802
+ -35.66286187147671,
6803
+ 93.75823172077577
6804
+ ],
6805
+ [
6806
+ -0.9613871411438595,
6807
+ 93.75823172077577
6808
+ ]
6809
+ ],
6810
+ "lastCommittedPoint": null,
6811
+ "startBinding": {
6812
+ "elementId": "-AKHuCF8e7h2_yxWV-Lgt",
6813
+ "fixedPoint": [
6814
+ -0.021111086046634895,
6815
+ 0.4980737633349352
6816
+ ],
6817
+ "focus": 0,
6818
+ "gap": 0
6819
+ },
6820
+ "endBinding": {
6821
+ "elementId": "zkIptPJSdTc7fyDulV4Nc",
6822
+ "fixedPoint": [
6823
+ -0.02242466713384723,
6824
+ 0.4977272727272722
6825
+ ],
6826
+ "focus": 0,
6827
+ "gap": 1
6828
+ },
6829
+ "startArrowhead": null,
6830
+ "endArrowhead": "arrow",
6831
+ "elbowed": true,
6832
+ "fixedSegments": null,
6833
+ "startIsSpecial": null,
6834
+ "endIsSpecial": null
6835
+ },
6836
+ {
6837
+ "id": "uuVJhiL1l0abY9oNZiD10",
6838
+ "type": "arrow",
6839
+ "x": 6954.423001342112,
6840
+ "y": 734.6366477728008,
6841
+ "width": 782.0369581018585,
6842
+ "height": 877.5492649993116,
6843
+ "angle": 0,
6844
+ "strokeColor": "#1e1e1e",
6845
+ "backgroundColor": "transparent",
6846
+ "fillStyle": "solid",
6847
+ "strokeWidth": 2,
6848
+ "strokeStyle": "solid",
6849
+ "roughness": 1,
6850
+ "opacity": 100,
6851
+ "groupIds": [],
6852
+ "frameId": null,
6853
+ "index": "b2Y",
6854
+ "roundness": null,
6855
+ "seed": 1564607801,
6856
+ "version": 173,
6857
+ "versionNonce": 1278975575,
6858
+ "isDeleted": false,
6859
+ "boundElements": [
6860
+ {
6861
+ "type": "text",
6862
+ "id": "_MRgU8xvGUK87XtjZCVdB"
6863
+ }
6864
+ ],
6865
+ "updated": 1741577087661,
6866
+ "link": null,
6867
+ "locked": false,
6868
+ "points": [
6869
+ [
6870
+ 0,
6871
+ 0
6872
+ ],
6873
+ [
6874
+ 766.9953999550553,
6875
+ 0
6876
+ ],
6877
+ [
6878
+ 766.9953999550553,
6879
+ -877.5492649993116
6880
+ ],
6881
+ [
6882
+ -15.041558146803254,
6883
+ -877.5492649993116
6884
+ ],
6885
+ [
6886
+ -15.041558146803254,
6887
+ -816.8845344422608
6888
+ ]
6889
+ ],
6890
+ "lastCommittedPoint": null,
6891
+ "startBinding": {
6892
+ "elementId": "hh-vJczGSvSCpkhjc-ZFd",
6893
+ "fixedPoint": [
6894
+ 1.077294685990338,
6895
+ 0.4977272727272722
6896
+ ],
6897
+ "focus": 0,
6898
+ "gap": 0
6899
+ },
6900
+ "endBinding": {
6901
+ "elementId": "5rQwEFQ0OLt7lCCh6ROrl",
6902
+ "fixedPoint": [
6903
+ 0.49959225118778955,
6904
+ -0.09631183325323865
6905
+ ],
6906
+ "focus": 0,
6907
+ "gap": 0
6908
+ },
6909
+ "startArrowhead": null,
6910
+ "endArrowhead": "arrow",
6911
+ "elbowed": true,
6912
+ "fixedSegments": [
6913
+ {
6914
+ "index": 2,
6915
+ "start": [
6916
+ 766.9953999550553,
6917
+ 0
6918
+ ],
6919
+ "end": [
6920
+ 766.9953999550553,
6921
+ -877.5492649993116
6922
+ ]
6923
+ },
6924
+ {
6925
+ "index": 3,
6926
+ "start": [
6927
+ 766.9953999550553,
6928
+ -877.5492649993116
6929
+ ],
6930
+ "end": [
6931
+ -15.041558146803254,
6932
+ -877.5492649993116
6933
+ ]
6934
+ }
6935
+ ],
6936
+ "startIsSpecial": false,
6937
+ "endIsSpecial": false
6938
+ },
6939
+ {
6940
+ "id": "_MRgU8xvGUK87XtjZCVdB",
6941
+ "type": "text",
6942
+ "x": 7603.199948843554,
6943
+ "y": -162.28761722651086,
6944
+ "width": 236.43690490722656,
6945
+ "height": 38.75,
6946
+ "angle": 0,
6947
+ "strokeColor": "#1e1e1e",
6948
+ "backgroundColor": "transparent",
6949
+ "fillStyle": "solid",
6950
+ "strokeWidth": 2,
6951
+ "strokeStyle": "solid",
6952
+ "roughness": 1,
6953
+ "opacity": 100,
6954
+ "groupIds": [],
6955
+ "frameId": null,
6956
+ "index": "b2Z",
6957
+ "roundness": null,
6958
+ "seed": 345379383,
6959
+ "version": 26,
6960
+ "versionNonce": 1436350681,
6961
+ "isDeleted": false,
6962
+ "boundElements": null,
6963
+ "updated": 1741577085801,
6964
+ "link": null,
6965
+ "locked": false,
6966
+ "text": "Research Query",
6967
+ "fontSize": 31,
6968
+ "fontFamily": 1,
6969
+ "textAlign": "center",
6970
+ "verticalAlign": "middle",
6971
+ "containerId": "uuVJhiL1l0abY9oNZiD10",
6972
+ "originalText": "Research Query",
6973
+ "autoResize": true,
6974
+ "lineHeight": 1.25
6975
+ },
6976
+ {
6977
+ "id": "RG4hRN73PDSPg9TvjEABI",
6978
+ "type": "arrow",
6979
+ "x": 6702.502185501241,
6980
+ "y": 636.8597301057443,
6981
+ "width": 177.233315840871,
6982
+ "height": 97.77691766705641,
6983
+ "angle": 0,
6984
+ "strokeColor": "#1e1e1e",
6985
+ "backgroundColor": "transparent",
6986
+ "fillStyle": "solid",
6987
+ "strokeWidth": 2,
6988
+ "strokeStyle": "solid",
6989
+ "roughness": 1,
6990
+ "opacity": 100,
6991
+ "groupIds": [],
6992
+ "frameId": null,
6993
+ "index": "b2a",
6994
+ "roundness": null,
6995
+ "seed": 316962359,
6996
+ "version": 30,
6997
+ "versionNonce": 1935335577,
6998
+ "isDeleted": false,
6999
+ "boundElements": [
7000
+ {
7001
+ "type": "text",
7002
+ "id": "p7DsZCuKDJztbRiwgmN-N"
7003
+ }
7004
+ ],
7005
+ "updated": 1741577120714,
7006
+ "link": null,
7007
+ "locked": false,
7008
+ "points": [
7009
+ [
7010
+ 0,
7011
+ 0
7012
+ ],
7013
+ [
7014
+ 0,
7015
+ 97.77691766705641
7016
+ ],
7017
+ [
7018
+ 177.233315840871,
7019
+ 97.77691766705641
7020
+ ]
7021
+ ],
7022
+ "lastCommittedPoint": null,
7023
+ "startBinding": {
7024
+ "elementId": "W854nPSz0p4lut302JyEo",
7025
+ "fixedPoint": [
7026
+ 0.49967859913202245,
7027
+ 1.0963118332532389
7028
+ ],
7029
+ "focus": 0,
7030
+ "gap": 0
7031
+ },
7032
+ "endBinding": {
7033
+ "elementId": "hh-vJczGSvSCpkhjc-ZFd",
7034
+ "fixedPoint": [
7035
+ -0.07729468599033816,
7036
+ 0.4977272727272722
7037
+ ],
7038
+ "focus": 0,
7039
+ "gap": 0
7040
+ },
7041
+ "startArrowhead": null,
7042
+ "endArrowhead": "arrow",
7043
+ "elbowed": true,
7044
+ "fixedSegments": null,
7045
+ "startIsSpecial": null,
7046
+ "endIsSpecial": null
7047
+ },
7048
+ {
7049
+ "id": "p7DsZCuKDJztbRiwgmN-N",
7050
+ "type": "text",
7051
+ "x": 6600.489734329366,
7052
+ "y": 719.0116477728008,
7053
+ "width": 204.02490234375,
7054
+ "height": 31.25,
7055
+ "angle": 0,
7056
+ "strokeColor": "#1e1e1e",
7057
+ "backgroundColor": "transparent",
7058
+ "fillStyle": "solid",
7059
+ "strokeWidth": 2,
7060
+ "strokeStyle": "solid",
7061
+ "roughness": 1,
7062
+ "opacity": 100,
7063
+ "groupIds": [],
7064
+ "frameId": null,
7065
+ "index": "b2b",
7066
+ "roundness": null,
7067
+ "seed": 701652887,
7068
+ "version": 22,
7069
+ "versionNonce": 973861687,
7070
+ "isDeleted": false,
7071
+ "boundElements": null,
7072
+ "updated": 1741577118639,
7073
+ "link": null,
7074
+ "locked": false,
7075
+ "text": "Delivers Content",
7076
+ "fontSize": 25,
7077
+ "fontFamily": 1,
7078
+ "textAlign": "center",
7079
+ "verticalAlign": "middle",
7080
+ "containerId": "RG4hRN73PDSPg9TvjEABI",
7081
+ "originalText": "Delivers Content",
7082
+ "autoResize": true,
7083
+ "lineHeight": 1.25
7084
+ },
7085
+ {
7086
+ "id": "upANbd9gbvPyDVaixKhNb",
7087
+ "type": "text",
7088
+ "x": 6551.937202258377,
7089
+ "y": -259.4249996465235,
7090
+ "width": 171.4839630126953,
7091
+ "height": 53.75,
7092
+ "angle": 0,
7093
+ "strokeColor": "#1e1e1e",
7094
+ "backgroundColor": "transparent",
7095
+ "fillStyle": "solid",
7096
+ "strokeWidth": 2,
7097
+ "strokeStyle": "solid",
7098
+ "roughness": 1,
7099
+ "opacity": 100,
7100
+ "groupIds": [],
7101
+ "frameId": null,
7102
+ "index": "b2c",
7103
+ "roundness": null,
7104
+ "seed": 631705881,
7105
+ "version": 58,
7106
+ "versionNonce": 1454583383,
7107
+ "isDeleted": false,
7108
+ "boundElements": null,
7109
+ "updated": 1741577165072,
7110
+ "link": null,
7111
+ "locked": false,
7112
+ "text": "Ex: AWS",
7113
+ "fontSize": 43,
7114
+ "fontFamily": 1,
7115
+ "textAlign": "left",
7116
+ "verticalAlign": "top",
7117
+ "containerId": null,
7118
+ "originalText": "Ex: AWS",
7119
+ "autoResize": true,
7120
+ "lineHeight": 1.25
7121
  }
7122
  ],
7123
  "appState": {
backend/.gitignore DELETED
@@ -1 +0,0 @@
1
- downloads/*
 
 
backend/app.py CHANGED
@@ -1,20 +1,21 @@
1
  # pip install asyncio eventlet
2
  # pip install google-genai beautifulsoup4 selenium newspaper3k lxml_html_clean
3
  from fastapi import FastAPI
 
4
  import socketio
5
  import json, logging
6
  from knet import KNet
7
  from scraper import CrawlForAIScraper, WebScraper
8
  from dotenv import load_dotenv
9
  load_dotenv()
10
- import asyncio
11
 
12
  # Configure logging
13
  logging.basicConfig(level=logging.INFO)
14
  logger = logging.getLogger(__name__)
15
 
16
  app = FastAPI()
17
- # Increased pingTimeout and added logger
 
18
  sio = socketio.AsyncServer(cors_allowed_origins="*", ping_timeout=60, ping_interval=10, async_mode="asgi")
19
  app.mount('/', socketio.ASGIApp(sio))
20
 
@@ -43,7 +44,7 @@ async def health_check(sid, data):
43
  @sio.event
44
  async def start_research(sid, data):
45
  try:
46
- data = json.loads(data)
47
  topic = data.get("topic")
48
  session_id = sid
49
  logger.info(f"Starting research for client {session_id} on topic: {topic}")
@@ -56,25 +57,19 @@ async def start_research(sid, data):
56
  logger.error(f"Error in progress callback: {str(e)}")
57
  raise e
58
 
59
- try:
60
- research_results = await knet.conduct_research(topic, progress_callback)
61
- logger.info(f"Research completed for topic: {topic}")
62
- await sio.emit("research_complete", research_results, room=session_id)
63
- except Exception as e:
64
- logger.error(f"Research error: {str(e)}")
65
- await sio.emit("error", {"message": str(e)}, room=session_id)
66
- raise e
67
 
68
  except Exception as e:
69
- logger.error(f"Error handling research request: {str(e)}")
70
- await sio.emit("error", {"message": str(e)}, room=sid)
71
- raise e
72
 
73
 
74
  @sio.event
75
  async def test(sid, data):
76
  print("Testing...")
77
- data = json.loads(data)
78
  res = await knet.scraper._scrape_page(data["url"])
79
  print(json.dumps(res, indent=2))
80
  await scraper_instance.close()
 
1
  # pip install asyncio eventlet
2
  # pip install google-genai beautifulsoup4 selenium newspaper3k lxml_html_clean
3
  from fastapi import FastAPI
4
+ from fastapi.middleware.cors import CORSMiddleware
5
  import socketio
6
  import json, logging
7
  from knet import KNet
8
  from scraper import CrawlForAIScraper, WebScraper
9
  from dotenv import load_dotenv
10
  load_dotenv()
 
11
 
12
  # Configure logging
13
  logging.basicConfig(level=logging.INFO)
14
  logger = logging.getLogger(__name__)
15
 
16
  app = FastAPI()
17
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
18
+
19
  sio = socketio.AsyncServer(cors_allowed_origins="*", ping_timeout=60, ping_interval=10, async_mode="asgi")
20
  app.mount('/', socketio.ASGIApp(sio))
21
 
 
44
  @sio.event
45
  async def start_research(sid, data):
46
  try:
47
+ data = json.loads(data) if type(data) != dict else data
48
  topic = data.get("topic")
49
  session_id = sid
50
  logger.info(f"Starting research for client {session_id} on topic: {topic}")
 
57
  logger.error(f"Error in progress callback: {str(e)}")
58
  raise e
59
 
60
+ research_results = await knet.conduct_research(topic, progress_callback)
61
+ logger.info(f"Research completed for topic: {topic}")
62
+ await sio.emit("research_complete", research_results, room=session_id)
 
 
 
 
 
63
 
64
  except Exception as e:
65
+ logger.error(f"Research error: {str(e)}")
66
+ await sio.emit("error", {"message": str(e)}, room=session_id)
 
67
 
68
 
69
  @sio.event
70
  async def test(sid, data):
71
  print("Testing...")
72
+ data = json.loads(data) if type(data) != dict else data
73
  res = await knet.scraper._scrape_page(data["url"])
74
  print(json.dumps(res, indent=2))
75
  await scraper_instance.close()
backend/knet.py CHANGED
@@ -1,4 +1,5 @@
1
- from typing import Dict, List, Optional, Any
 
2
  import google.generativeai as genai
3
  from google.ai.generativelanguage_v1beta.types import content
4
  import logging
@@ -8,7 +9,6 @@ from datetime import datetime
8
  from dotenv import load_dotenv
9
  from research_node import ResearchNode
10
  from collections import deque
11
- import asyncio
12
 
13
  # Load environment variables
14
  load_dotenv()
@@ -36,15 +36,40 @@ class KNet:
36
  genai.configure(api_key=self.api_key)
37
 
38
  # Keep both models with original configurations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  self.llm = genai.GenerativeModel(
40
  "gemini-2.0-flash-lite-preview-02-05",
41
- generation_config={"temperature": 0.7},
 
42
  )
43
  self.ctx_researcher = []
44
 
45
  self.research_manager = genai.GenerativeModel(
46
  "gemini-2.0-flash-lite-preview-02-05",
47
- generation_config={"temperature": 0.3},
 
48
  )
49
  self.ctx_manager = []
50
 
@@ -118,30 +143,37 @@ class KNet:
118
  def _track_tokens(self, tokens: int) -> None:
119
  self.token_count += tokens
120
 
121
- def _should_branch_deeper(self, node: ResearchNode, topic: str) -> bool:
122
- # Generate summary of key findings into research_manager's context
123
- if node.data:
124
- findings = ("\n" + "-"*10 + "Next data" + "-"*10 + "\n").join([json.dumps(d, indent=2) for d in node.data])
125
- response = self.llm.generate_content(f"Extract key findings from the following data related to the topic '{topic}':\n{findings}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  self._track_tokens(response.usage_metadata.total_token_count)
127
- findings = response.text
128
- self.ctx_manager.append(findings)
129
-
130
- # Research manager takes decision to proceed or not
131
- prompt = self.branch_decision_prompt.format(
132
- query=node.query,
133
- depth=node.depth,
134
- path=" -> ".join(node.get_path_to_root()),
135
- findings="\n".join(self.ctx_manager),
136
- )
137
- response = self.research_manager.generate_content(
138
- prompt, generation_config={**self.branch_schema}
139
- )
140
- self._track_tokens(response.usage_metadata.total_token_count)
141
- result = json.loads(response.text)
142
- self.logger.info(f"Branch decision for '{node.query}': {result['decision']}")
143
 
144
- return result["decision"]
 
 
 
 
 
 
145
 
146
  async def conduct_research(self, topic: str, progress_callback=None) -> Dict[str, Any]:
147
  self.token_count = 0
@@ -184,7 +216,7 @@ class KNet:
184
  self.logger.info(f"Research completed. Explored {len(explored_queries)} queries across {root_node.max_depth()} levels")
185
  await progress.update(100, "Research complete!")
186
 
187
- with open("output.json", "w") as f:
188
  json.dump(final_report, f, indent=2)
189
  return final_report
190
 
@@ -192,23 +224,23 @@ class KNet:
192
  self.logger.error(f"Research failed: {str(e)}")
193
  raise e
194
 
195
- def _analyze_and_branch(self, node: ResearchNode, topic: str) -> List[ResearchNode]:
196
- if not node.data:
197
- return []
 
198
 
199
- analysis_prompt = f"""Based on the following findings about "{topic}", suggest new research directions.
200
- Findings:
201
- {json.dumps(self.ctx_manager, indent=2)}
202
 
203
- Suggest up to {self.max_breadth} specific google search queries that would help data which:
204
- - Builds upon these findings
205
- - Explores different aspects
206
- - Goes deeper into important details
207
 
208
- Return as JSON array of objects with properties:
209
- - query (string)"""
210
 
211
- try:
212
  response = self.research_manager.generate_content(
213
  analysis_prompt, generation_config={**self.analysis_schema}
214
  )
@@ -227,56 +259,65 @@ class KNet:
227
  return new_nodes
228
 
229
  except Exception as e:
 
 
 
230
  self.logger.error(f"Branch analysis failed: {str(e)}")
231
- return []
232
-
233
- def _generate_final_report(self, root_node: ResearchNode) -> Dict[str, Any]:
234
- findings = "\n".join(self.ctx_manager)
235
- print(f"""----------------- Findings -----------------""")
236
- print(findings)
237
- print(f"""----------------- Findings -----------------""")
238
- prompt = f"""Generate a comprehensive report on the topic "{root_node.query}" based on the following research findings:
239
- {findings}
240
- """
241
- response = self.research_manager.generate_content(prompt)
242
- self._track_tokens(response.usage_metadata.total_token_count)
243
-
244
- # Collate multimedia content
245
- media_content = {"images": [], "videos": [], "links": [], "references": []}
246
- all_sources_data = root_node.get_all_data()
247
- for data in all_sources_data:
248
- if data.get("images"):
249
- media_content["images"].extend(data["images"])
250
- if data.get("videos"):
251
- media_content["videos"].extend(data["videos"])
252
- if data.get("links"):
253
- media_content["links"].extend([{"url": l["href"], "text": l["text"]} for l in data["links"]])
254
- # Deduplicate
255
- media_content["images"] = list(set(media_content["images"]))
256
- media_content["videos"] = list(set(media_content["videos"]))
257
- media_content["links"] = list({json.dumps(d, sort_keys=True) for d in media_content["links"]})
258
- media_content["links"] = [json.loads(d) for d in media_content["links"]]
259
-
260
- # Build research tree structure
261
- def build_tree_structure(node: ResearchNode) -> Dict:
262
- if not node:
263
- return {}
 
 
 
 
 
 
264
  return {
265
- "query": node.query,
266
- "depth": node.depth,
267
- "children": [build_tree_structure(child) for child in node.children],
 
 
 
 
 
 
 
 
268
  }
269
-
270
- return {
271
- "topic": root_node.query,
272
- "timestamp": datetime.now().isoformat(),
273
- "content": response.text,
274
- "media": media_content,
275
- "research_tree": build_tree_structure(root_node),
276
- "metadata": {
277
- "total_queries": root_node.total_children(),
278
- "total_sources": len(all_sources_data),
279
- "max_depth_reached": root_node.max_depth(),
280
- "total_tokens": self.token_count,
281
- },
282
- }
 
1
+ from typing import Dict, List, Any
2
+ from textwrap import dedent
3
  import google.generativeai as genai
4
  from google.ai.generativelanguage_v1beta.types import content
5
  import logging
 
9
  from dotenv import load_dotenv
10
  from research_node import ResearchNode
11
  from collections import deque
 
12
 
13
  # Load environment variables
14
  load_dotenv()
 
36
  genai.configure(api_key=self.api_key)
37
 
38
  # Keep both models with original configurations
39
+ generation_config = {"temperature": 0.9}
40
+ safe = [
41
+ {
42
+ "category": "HARM_CATEGORY_DANGEROUS",
43
+ "threshold": "BLOCK_NONE",
44
+ },
45
+ {
46
+ "category": "HARM_CATEGORY_HARASSMENT",
47
+ "threshold": "BLOCK_NONE",
48
+ },
49
+ {
50
+ "category": "HARM_CATEGORY_HATE_SPEECH",
51
+ "threshold": "BLOCK_NONE",
52
+ },
53
+ {
54
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
55
+ "threshold": "BLOCK_NONE",
56
+ },
57
+ {
58
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
59
+ "threshold": "BLOCK_NONE",
60
+ },
61
+ ]
62
  self.llm = genai.GenerativeModel(
63
  "gemini-2.0-flash-lite-preview-02-05",
64
+ generation_config=generation_config,
65
+ safety_settings=safe,
66
  )
67
  self.ctx_researcher = []
68
 
69
  self.research_manager = genai.GenerativeModel(
70
  "gemini-2.0-flash-lite-preview-02-05",
71
+ generation_config=generation_config,
72
+ safety_settings=safe,
73
  )
74
  self.ctx_manager = []
75
 
 
143
  def _track_tokens(self, tokens: int) -> None:
144
  self.token_count += tokens
145
 
146
+ def _should_branch_deeper(self, node: ResearchNode, topic: str, retry_count=0) -> bool:
147
+ try:
148
+ # Generate summary of key findings into research_manager's context
149
+ if node.data:
150
+ findings = ("\n" + "-"*10 + "Next data" + "-"*10 + "\n").join([json.dumps(d, indent=2) for d in node.data])
151
+ response = self.llm.generate_content(f"Extract key findings from the following data related to the topic '{topic}':\n{findings}")
152
+ self._track_tokens(response.usage_metadata.total_token_count)
153
+ findings = response.text
154
+ self.ctx_manager.append(findings)
155
+
156
+ # Research manager takes decision to proceed or not
157
+ prompt = self.branch_decision_prompt.format(
158
+ query=node.query,
159
+ depth=node.depth,
160
+ path=" -> ".join(node.get_path_to_root()),
161
+ findings="\n".join(self.ctx_manager),
162
+ )
163
+ response = self.research_manager.generate_content(
164
+ prompt, generation_config={**self.branch_schema}
165
+ )
166
  self._track_tokens(response.usage_metadata.total_token_count)
167
+ result = json.loads(response.text)
168
+ self.logger.info(f"Branch decision for '{node.query}': {result['decision']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ return result["decision"]
171
+ except Exception as e:
172
+ if result["candidates"][0]["finishReason"] == "RECITATION":
173
+ self.logger.error(f"Retrying branch decision: {str(e)}\nC:{retry_count/3}")
174
+ self._should_branch_deeper(node, topic, retry_count+1)
175
+ self.logger.error(f"Branch decision failed: {str(e)}")
176
+ raise e
177
 
178
  async def conduct_research(self, topic: str, progress_callback=None) -> Dict[str, Any]:
179
  self.token_count = 0
 
216
  self.logger.info(f"Research completed. Explored {len(explored_queries)} queries across {root_node.max_depth()} levels")
217
  await progress.update(100, "Research complete!")
218
 
219
+ with open("output.json", "a") as f:
220
  json.dump(final_report, f, indent=2)
221
  return final_report
222
 
 
224
  self.logger.error(f"Research failed: {str(e)}")
225
  raise e
226
 
227
+ def _analyze_and_branch(self, node: ResearchNode, topic: str, retry_count=0) -> List[ResearchNode]:
228
+ try:
229
+ if not node.data:
230
+ return []
231
 
232
+ analysis_prompt = dedent(f"""Based on the following findings about "{topic}", suggest new research directions.
233
+ Findings:
234
+ {json.dumps(self.ctx_manager, indent=2)}
235
 
236
+ Suggest up to {self.max_breadth} specific google search queries that would help data which:
237
+ - Builds upon these findings
238
+ - Explores different aspects
239
+ - Goes deeper into important details
240
 
241
+ Return as JSON array of objects with properties:
242
+ - query (string)""")
243
 
 
244
  response = self.research_manager.generate_content(
245
  analysis_prompt, generation_config={**self.analysis_schema}
246
  )
 
259
  return new_nodes
260
 
261
  except Exception as e:
262
+ if result["candidates"][0]["finishReason"] == "RECITATION" and retry_count <= 3:
263
+ self.logger.error(f"Retrying analysis: {str(e)}\nC:{retry_count/3}")
264
+ self._analyze_and_branch(node, topic, retry_count+1)
265
  self.logger.error(f"Branch analysis failed: {str(e)}")
266
+ raise e
267
+
268
+ def _generate_final_report(self, root_node: ResearchNode, retry_count=0) -> Dict[str, Any]:
269
+ try:
270
+ findings = "\n".join(self.ctx_manager)
271
+ with open("output.json", "w") as f:
272
+ f.write(findings)
273
+ prompt = f"""Generate a comprehensive report on the topic "{root_node.query}" based on the following research findings:
274
+ {findings}
275
+ """
276
+ response = self.research_manager.generate_content(prompt)
277
+ self._track_tokens(response.usage_metadata.total_token_count)
278
+
279
+ # Collate multimedia content
280
+ media_content = {"images": [], "videos": [], "links": [], "references": []}
281
+ all_sources_data = root_node.get_all_data()
282
+ for data in all_sources_data:
283
+ if data.get("images"):
284
+ media_content["images"].extend(data["images"])
285
+ if data.get("videos"):
286
+ media_content["videos"].extend(data["videos"])
287
+ if data.get("links"):
288
+ media_content["links"].extend([{"url": l["href"], "text": l["text"]} for l in data["links"]])
289
+ # Deduplicate
290
+ media_content["images"] = list(set(media_content["images"]))
291
+ media_content["videos"] = list(set(media_content["videos"]))
292
+ media_content["links"] = list({json.dumps(d, sort_keys=True) for d in media_content["links"]})
293
+ media_content["links"] = [json.loads(d) for d in media_content["links"]]
294
+
295
+ # Build research tree structure
296
+ def build_tree_structure(node: ResearchNode) -> Dict:
297
+ if not node:
298
+ return {}
299
+ return {
300
+ "query": node.query,
301
+ "depth": node.depth,
302
+ "children": [build_tree_structure(child) for child in node.children],
303
+ }
304
+
305
  return {
306
+ "topic": root_node.query,
307
+ "timestamp": datetime.now().isoformat(),
308
+ "content": response.text,
309
+ "media": media_content,
310
+ "research_tree": build_tree_structure(root_node),
311
+ "metadata": {
312
+ "total_queries": root_node.total_children(),
313
+ "total_sources": len(all_sources_data),
314
+ "max_depth_reached": root_node.max_depth(),
315
+ "total_tokens": self.token_count,
316
+ },
317
  }
318
+ except Exception as e:
319
+ if response["candidates"][0]["finishReason"] == "RECITATION":
320
+ self.logger.error(f"Retrying final report: {str(e)}\nC:{retry_count/3}")
321
+ self._generate_final_report(root_node, retry_count+1)
322
+ self.logger.error(f"Error generating final report: {str(e)}")
323
+ raise e
 
 
 
 
 
 
 
 
backend/output.json DELETED
The diff for this file is too large to render. See raw diff
 
frontend/bun.lock CHANGED
The diff for this file is too large to render. See raw diff
 
frontend/components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
frontend/package.json CHANGED
@@ -9,16 +9,40 @@
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  "react": "^18",
13
  "react-dom": "^18",
14
- "next": "14.2.24"
 
 
 
 
 
 
 
15
  },
16
  "devDependencies": {
17
- "typescript": "^5",
18
  "@types/node": "^20",
19
  "@types/react": "^18",
20
  "@types/react-dom": "^18",
 
21
  "postcss": "^8",
22
- "tailwindcss": "^3.4.1"
 
23
  }
24
  }
 
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
+ "@radix-ui/react-avatar": "^1.1.3",
13
+ "@radix-ui/react-checkbox": "^1.1.4",
14
+ "@radix-ui/react-dialog": "^1.1.6",
15
+ "@radix-ui/react-dropdown-menu": "^2.1.6",
16
+ "@radix-ui/react-label": "^2.1.2",
17
+ "@radix-ui/react-scroll-area": "^1.2.3",
18
+ "@radix-ui/react-select": "^2.1.6",
19
+ "@radix-ui/react-separator": "^1.1.2",
20
+ "@radix-ui/react-slot": "^1.1.2",
21
+ "@radix-ui/react-tabs": "^1.1.3",
22
+ "@radix-ui/react-tooltip": "^1.1.8",
23
+ "class-variance-authority": "^0.7.1",
24
+ "clsx": "^2.1.1",
25
+ "lucide-react": "^0.479.0",
26
+ "next": "14.2.24",
27
+ "next-themes": "^0.4.5",
28
  "react": "^18",
29
  "react-dom": "^18",
30
+ "react-markdown": "^10.1.0",
31
+ "react-resizable-panels": "^2.1.7",
32
+ "remark-gfm": "^4.0.1",
33
+ "shadcn": "^2.4.0-canary.12",
34
+ "socket.io-client": "^4.8.1",
35
+ "tailwind-merge": "^3.0.2",
36
+ "tailwindcss-animate": "^1.0.7",
37
+ "uuid": "^11.1.0"
38
  },
39
  "devDependencies": {
 
40
  "@types/node": "^20",
41
  "@types/react": "^18",
42
  "@types/react-dom": "^18",
43
+ "@types/uuid": "^10.0.0",
44
  "postcss": "^8",
45
+ "tailwindcss": "^3.4.1",
46
+ "typescript": "^5"
47
  }
48
  }
frontend/src/app/fonts/GeistMonoVF.woff DELETED
Binary file (67.9 kB)
 
frontend/src/app/fonts/GeistVF.woff DELETED
Binary file (66.3 kB)
 
frontend/src/app/globals.css CHANGED
@@ -2,26 +2,78 @@
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
- :root {
6
- --background: #ffffff;
7
- --foreground: #171717;
 
8
  }
9
 
10
- @media (prefers-color-scheme: dark) {
 
11
  :root {
12
- --background: #0a0a0a;
13
- --foreground: #ededed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
- }
16
 
17
- body {
18
- color: var(--foreground);
19
- background: var(--background);
20
- font-family: Arial, Helvetica, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
22
 
23
- @layer utilities {
24
- .text-balance {
25
- text-wrap: balance;
 
 
26
  }
27
- }
 
 
 
 
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
+ @layer utilities {
6
+ .text-balance {
7
+ text-wrap: balance;
8
+ }
9
  }
10
 
11
+
12
+ @layer base {
13
  :root {
14
+ --background: 0 0% 100%;
15
+ --foreground: 222.2 84% 4.9%;
16
+ --card: 0 0% 100%;
17
+ --card-foreground: 222.2 84% 4.9%;
18
+ --popover: 0 0% 100%;
19
+ --popover-foreground: 222.2 84% 4.9%;
20
+ --primary: 222.2 47.4% 11.2%;
21
+ --primary-foreground: 210 40% 98%;
22
+ --secondary: 210 40% 96.1%;
23
+ --secondary-foreground: 222.2 47.4% 11.2%;
24
+ --muted: 210 40% 96.1%;
25
+ --muted-foreground: 215.4 16.3% 46.9%;
26
+ --accent: 210 40% 96.1%;
27
+ --accent-foreground: 222.2 47.4% 11.2%;
28
+ --destructive: 0 84.2% 60.2%;
29
+ --destructive-foreground: 210 40% 98%;
30
+ --border: 214.3 31.8% 91.4%;
31
+ --input: 214.3 31.8% 91.4%;
32
+ --ring: 222.2 84% 4.9%;
33
+ --chart-1: 12 76% 61%;
34
+ --chart-2: 173 58% 39%;
35
+ --chart-3: 197 37% 24%;
36
+ --chart-4: 43 74% 66%;
37
+ --chart-5: 27 87% 67%;
38
+ --radius: 0.5rem;
39
  }
 
40
 
41
+ .dark {
42
+ --background: 222.2 84% 4.9%;
43
+ --foreground: 210 40% 98%;
44
+ --card: 222.2 84% 4.9%;
45
+ --card-foreground: 210 40% 98%;
46
+ --popover: 222.2 84% 4.9%;
47
+ --popover-foreground: 210 40% 98%;
48
+ --primary: 210 40% 98%;
49
+ --primary-foreground: 222.2 47.4% 11.2%;
50
+ --secondary: 217.2 32.6% 17.5%;
51
+ --secondary-foreground: 210 40% 98%;
52
+ --muted: 217.2 32.6% 17.5%;
53
+ --muted-foreground: 215 20.2% 65.1%;
54
+ --accent: 217.2 32.6% 17.5%;
55
+ --accent-foreground: 210 40% 98%;
56
+ --destructive: 0 62.8% 30.6%;
57
+ --destructive-foreground: 210 40% 98%;
58
+ --border: 217.2 32.6% 17.5%;
59
+ --input: 217.2 32.6% 17.5%;
60
+ --ring: 212.7 26.8% 83.9%;
61
+ --chart-1: 220 70% 50%;
62
+ --chart-2: 160 60% 45%;
63
+ --chart-3: 30 80% 55%;
64
+ --chart-4: 280 65% 60%;
65
+ --chart-5: 340 75% 55%;
66
+ }
67
  }
68
 
69
+
70
+
71
+ @layer base {
72
+ * {
73
+ @apply border-border;
74
  }
75
+
76
+ body {
77
+ @apply bg-background text-foreground;
78
+ }
79
+ }
frontend/src/app/layout.tsx CHANGED
@@ -1,34 +1,19 @@
 
1
  import type { Metadata } from "next";
2
- import localFont from "next/font/local";
3
  import "./globals.css";
4
 
5
- const geistSans = localFont({
6
- src: "./fonts/GeistVF.woff",
7
- variable: "--font-geist-sans",
8
- weight: "100 900",
9
- });
10
- const geistMono = localFont({
11
- src: "./fonts/GeistMonoVF.woff",
12
- variable: "--font-geist-mono",
13
- weight: "100 900",
14
- });
15
-
16
  export const metadata: Metadata = {
17
- title: "Create Next App",
18
- description: "Generated by create next app",
19
  };
20
 
21
- export default function RootLayout({
22
- children,
23
- }: Readonly<{
24
- children: React.ReactNode;
25
- }>) {
26
  return (
27
- <html lang="en">
28
- <body
29
- className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30
- >
31
- {children}
32
  </body>
33
  </html>
34
  );
 
1
+ import { ThemeProvider } from "@/components/theme-provider";
2
  import type { Metadata } from "next";
 
3
  import "./globals.css";
4
 
 
 
 
 
 
 
 
 
 
 
 
5
  export const metadata: Metadata = {
6
+ title: "Research Chatbot",
7
+ description: "Deep research assistant with real-time updates",
8
  };
9
 
10
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
 
 
 
 
11
  return (
12
+ <html lang="en" suppressHydrationWarning>
13
+ <body>
14
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
15
+ {children}
16
+ </ThemeProvider>
17
  </body>
18
  </html>
19
  );
frontend/src/app/page.tsx CHANGED
@@ -1,5 +1,5 @@
 
 
1
  export default function Home() {
2
- return (
3
- <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"></div>
4
- );
5
  }
 
1
+ import ChatInterface from '@/components/ChatInterface';
2
+
3
  export default function Home() {
4
+ return <ChatInterface />;
 
 
5
  }
frontend/src/components/ChatHistory.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { ScrollArea } from "@/components/ui/scroll-area";
3
+ import { Message as MessageType } from "@/lib/types";
4
+ import { Loader2 } from "lucide-react";
5
+ import React, { useEffect, useRef } from "react";
6
+ import Message from "./Message";
7
+
8
+ interface ChatHistoryProps {
9
+ messages: MessageType[];
10
+ isLoading: boolean;
11
+ }
12
+
13
+ const ChatHistory: React.FC<ChatHistoryProps> = ({ messages, isLoading }) => {
14
+ const messagesEndRef = useRef<HTMLDivElement>(null);
15
+
16
+ useEffect(() => {
17
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
18
+ }, [messages]);
19
+
20
+ return (
21
+ <ScrollArea tabIndex={-1}>
22
+ <div className="pb-24 focus-visible:outline-0" tabIndex={-1}>
23
+ {messages.length === 0 ? (
24
+ <div className="h-full flex flex-col items-center justify-center p-8 text-center">
25
+ <div className="max-w-md space-y-2">
26
+ <h2 className="text-2xl font-bold">Welcome to Deep Research</h2>
27
+ <p className="text-muted-foreground">Ask any research question to get started. The assistant will provide detailed answers backed by research and sources.</p>
28
+ </div>
29
+ </div>
30
+ ) : (
31
+ messages.map((message) => <Message key={message.id} message={message} />)
32
+ )}
33
+
34
+ {isLoading && (
35
+ <div className="my-2 mx-4">
36
+ <div className="max-w-2xl mx-auto flex gap-4 relative">
37
+ <div className="h-8 w-8 rounded-full shrink-0 bg-muted flex items-center justify-center absolute -left-12 top-0">
38
+ <Loader2 className="h-5 w-5 animate-spin" />
39
+ </div>
40
+ <div className="flex-1">
41
+ <div className="flex items-center gap-2 mb-1">
42
+ <div className="text-xs text-muted-foreground">Just now</div>
43
+ <div className="font-medium">Research Assistant</div>
44
+ </div>
45
+ <div className="mt-1 bg-muted/50 p-3 rounded-2xl rounded-tl-sm">
46
+ <div className="flex space-x-2">
47
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/30 animate-pulse"></div>
48
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/30 animate-pulse" style={{ animationDelay: "300ms" }}></div>
49
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/30 animate-pulse" style={{ animationDelay: "600ms" }}></div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ )}
56
+
57
+ <div ref={messagesEndRef} />
58
+ </div>
59
+ </ScrollArea>
60
+ );
61
+ };
62
+
63
+ export default ChatHistory;
frontend/src/components/ChatInterface.tsx ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
3
+ import { disconnectSocket, getSocket, initializeSocket } from "@/lib/socket";
4
+ import { ChatData, ChatState, Conversation, Message, ResearchOptions, ResearchResults, StatusUpdate } from "@/lib/types";
5
+ import { useEffect, useState } from "react";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import ChatHistory from "./ChatHistory";
8
+ import MessageInput from "./MessageInput";
9
+ import ResearchControls from "./ResearchControls";
10
+ import ChatLayout from "./ui/ChatLayout";
11
+ import ConversationList from "./ui/ConversationList";
12
+
13
+ const saveToStorage = (data: ChatData) => {
14
+ if (typeof window !== "undefined") {
15
+ localStorage.setItem("chatData", JSON.stringify(data));
16
+ }
17
+ };
18
+
19
+ const loadFromStorage = (): ChatData => {
20
+ if (typeof window === "undefined") {
21
+ return { conversations: [], currentConversationId: null };
22
+ }
23
+ const data = localStorage.getItem("chatData");
24
+ if (!data) {
25
+ return { conversations: [], currentConversationId: null };
26
+ }
27
+ try {
28
+ const parsed = JSON.parse(data);
29
+ return {
30
+ conversations: Array.isArray(parsed.conversations) ? parsed.conversations : [],
31
+ currentConversationId: parsed.currentConversationId,
32
+ };
33
+ } catch (e) {
34
+ return { conversations: [], currentConversationId: null };
35
+ }
36
+ };
37
+
38
+ const ChatInterface = () => {
39
+ const [chatState, setChatState] = useState<ChatState>({ messages: [], isLoading: false, error: null });
40
+ const [conversations, setConversations] = useState<Conversation[]>([]);
41
+ const [currentConversationId, setCurrentConversationId] = useState<string | null>(null);
42
+
43
+ const [researchOptions, setResearchOptions] = useState<ResearchOptions>({
44
+ depth: "basic",
45
+ sources: true,
46
+ citations: false,
47
+ });
48
+
49
+ // Initialize socket once
50
+ useEffect(() => {
51
+ const socket = initializeSocket();
52
+
53
+ socket.on("connect", () => {
54
+ console.log("Connected to research server");
55
+ });
56
+
57
+ socket.on("disconnect", () => {
58
+ console.log("Disconnected from research server");
59
+ });
60
+
61
+ socket.on("status", (data: StatusUpdate) => {
62
+ setChatState((prevState) => {
63
+ const messages = [...prevState.messages];
64
+ const progressText = `(${data.progress}%) ${data.message}`;
65
+
66
+ // Find the last assistant message that is a progress update
67
+ const lastProgressIndex = messages.findLastIndex((msg) => msg.role === "assistant" && msg.content.includes("%)"));
68
+
69
+ if (lastProgressIndex !== -1) {
70
+ // Update existing progress message
71
+ messages[lastProgressIndex] = {
72
+ ...messages[lastProgressIndex],
73
+ content: progressText,
74
+ };
75
+ } else {
76
+ // Add new progress message
77
+ messages.push({
78
+ id: uuidv4(),
79
+ content: progressText,
80
+ role: "assistant",
81
+ timestamp: new Date(),
82
+ });
83
+ }
84
+
85
+ return {
86
+ ...prevState,
87
+ messages,
88
+ isLoading: true,
89
+ };
90
+ });
91
+ });
92
+
93
+ socket.on("research_complete", (results: ResearchResults) => {
94
+ setChatState((prevState) => {
95
+ const messages = [...prevState.messages];
96
+
97
+ // Remove the last progress message if it exists
98
+ const lastProgressIndex = messages.findLastIndex((msg) => msg.role === "assistant" && msg.content.includes("%)"));
99
+ if (lastProgressIndex !== -1) {
100
+ messages.splice(lastProgressIndex, 1);
101
+ }
102
+
103
+ // Format research stats and response
104
+ const stats = [`Total Queries: ${results.metadata.total_queries}`, `Sources Used: ${results.metadata.total_sources}`, `Search Depth: ${results.metadata.max_depth_reached}`].join(" | ");
105
+
106
+ const formattedResponse = [results.content, `\n\n---\n**Research Stats:**\n${stats}`, results.media?.images?.length ? `\n\n**Relevant Images:**\n${results.media.images.join("\n")}` : ""].join("");
107
+
108
+ const newMessages = [
109
+ ...messages,
110
+ {
111
+ id: uuidv4(),
112
+ content: formattedResponse,
113
+ role: "assistant" as const,
114
+ timestamp: new Date(results.timestamp),
115
+ },
116
+ ];
117
+
118
+ return {
119
+ ...prevState,
120
+ isLoading: false,
121
+ messages: newMessages,
122
+ };
123
+ });
124
+ });
125
+
126
+ socket.on("error", (error: { message: string }) => {
127
+ setChatState((prevState) => ({
128
+ ...prevState,
129
+ error: error.message,
130
+ isLoading: false,
131
+ }));
132
+ });
133
+
134
+ return () => {
135
+ disconnectSocket();
136
+ };
137
+ }, []); // Empty dependency array
138
+
139
+ // Load initial data
140
+ useEffect(() => {
141
+ const data = loadFromStorage();
142
+ setConversations(data.conversations);
143
+ setCurrentConversationId(data.currentConversationId);
144
+
145
+ if (data.currentConversationId) {
146
+ const currentConv = data.conversations.find((c) => c.id === data.currentConversationId);
147
+ if (currentConv) {
148
+ setChatState((prev) => ({ ...prev, messages: currentConv.messages }));
149
+ }
150
+ }
151
+ }, []);
152
+
153
+ useEffect(() => {
154
+ const currentConv = conversations.find((c) => c.id === currentConversationId);
155
+
156
+ const data: ChatData = {
157
+ conversations: conversations.map((conv) => ({
158
+ ...conv,
159
+ messages: conv.id === currentConversationId ? chatState.messages : conv.messages || [],
160
+ })),
161
+ currentConversationId,
162
+ };
163
+
164
+ saveToStorage(data);
165
+ }, [conversations, currentConversationId, chatState.messages]);
166
+
167
+ const handleSendMessage = (content: string) => {
168
+ if (!content.trim()) return;
169
+
170
+ let conversationId = currentConversationId;
171
+ const newMessage: Message = {
172
+ id: uuidv4(),
173
+ content,
174
+ role: "user",
175
+ timestamp: new Date(),
176
+ };
177
+
178
+ // Create a new conversation if none exists
179
+ if (!conversationId) {
180
+ conversationId = uuidv4();
181
+ setCurrentConversationId(conversationId);
182
+ setConversations((prev) => [
183
+ {
184
+ id: conversationId as string,
185
+ title: content.length > 30 ? `${content.substring(0, 30)}...` : content,
186
+ lastUpdated: new Date().toISOString(),
187
+ messages: [newMessage],
188
+ active: true,
189
+ },
190
+ ...prev.map((c) => ({ ...c, active: false })),
191
+ ]);
192
+ } else {
193
+ // Update the existing conversation
194
+ setConversations((prev) =>
195
+ prev.map((conv) => ({
196
+ ...conv,
197
+ lastUpdated: conv.id === conversationId ? new Date().toISOString() : conv.lastUpdated,
198
+ active: conv.id === conversationId,
199
+ messages: conv.id === conversationId ? [...(conv.messages || []), newMessage] : conv.messages || [],
200
+ }))
201
+ );
202
+ }
203
+
204
+ setChatState((prevState) => ({
205
+ ...prevState,
206
+ messages: [...prevState.messages, newMessage],
207
+ isLoading: true,
208
+ error: null,
209
+ }));
210
+
211
+ // Send message to server via socket
212
+ try {
213
+ const socket = getSocket();
214
+ socket.emit("start_research", {
215
+ topic: content,
216
+ });
217
+ } catch (error) {
218
+ setChatState((prevState) => ({
219
+ ...prevState,
220
+ error: "Failed to connect to research server",
221
+ isLoading: false,
222
+ }));
223
+ }
224
+ };
225
+
226
+ const handleNewConversation = () => {
227
+ setCurrentConversationId(null);
228
+ setChatState({
229
+ messages: [],
230
+ isLoading: false,
231
+ error: null,
232
+ });
233
+ };
234
+
235
+ const handleSelectConversation = (id: string) => {
236
+ const data = loadFromStorage();
237
+ const conversation = data.conversations.find((c) => c.id === id);
238
+
239
+ setCurrentConversationId(id);
240
+ setChatState((prev) => ({
241
+ ...prev,
242
+ messages: conversation?.messages || [],
243
+ isLoading: false,
244
+ error: null,
245
+ }));
246
+ setConversations((prev) =>
247
+ prev.map((conv) => ({
248
+ ...conv,
249
+ active: conv.id === id,
250
+ }))
251
+ );
252
+ };
253
+
254
+ const sidebar = <ConversationList conversations={conversations} onNewConversation={handleNewConversation} onSelectConversation={handleSelectConversation} />;
255
+
256
+ const mainContent = (
257
+ <div className="flex flex-col w-full h-full relative" tabIndex={-1}>
258
+ <ChatHistory messages={chatState.messages} isLoading={chatState.isLoading} />
259
+
260
+ {chatState.error && (
261
+ <Alert variant="destructive" className="mx-2 my-2 mb-28 w-90 bg-red-950 text-red-50">
262
+ <AlertTitle>Error</AlertTitle>
263
+ <AlertDescription>{chatState.error}</AlertDescription>
264
+ </Alert>
265
+ )}
266
+
267
+ <MessageInput onSendMessage={handleSendMessage} isLoading={chatState.isLoading} />
268
+ </div>
269
+ );
270
+
271
+ return <ChatLayout sidebar={sidebar} mainContent={mainContent} settingsPanel={<ResearchControls options={researchOptions} onOptionChange={setResearchOptions} />} />;
272
+ };
273
+
274
+ export default ChatInterface;
frontend/src/components/Message.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Avatar } from "@/components/ui/avatar";
2
+ import { Button } from "@/components/ui/button";
3
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
4
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
5
+ import { Message as MessageType } from "@/lib/types";
6
+ import { Bot, Copy, MoreHorizontal, User2 } from "lucide-react";
7
+ import React from "react";
8
+ import ReactMarkdown from "react-markdown";
9
+ import remarkGfm from "remark-gfm";
10
+
11
+ const MarkdownComponents: Record<string, React.ComponentType<any>> = {
12
+ h1: ({ children }) => <h1 className="text-2xl font-bold mb-4">{children}</h1>,
13
+ h2: ({ children }) => <h2 className="text-xl font-bold mb-3">{children}</h2>,
14
+ h3: ({ children }) => <h3 className="text-lg font-bold mb-3">{children}</h3>,
15
+ p: ({ children }) => <p className="mb-4 last:mb-0">{children}</p>,
16
+ ul: ({ children }) => <ul className="list-disc ml-6 mb-4">{children}</ul>,
17
+ ol: ({ children }) => <ol className="list-decimal ml-6 mb-4">{children}</ol>,
18
+ li: ({ children }) => <li className="mb-1">{children}</li>,
19
+ code: ({ node, inline, className, children, ...props }) => (
20
+ <code className={`${inline ? "bg-muted px-1 py-0.5 rounded-md text-sm" : "block bg-muted/50 p-3 rounded-lg text-sm overflow-x-auto"}`} {...props}>
21
+ {children}
22
+ </code>
23
+ ),
24
+ pre: ({ children }) => <pre className="bg-transparent p-0">{children}</pre>,
25
+ a: ({ children, href }) => (
26
+ <a href={href} className="text-primary hover:underline" target="_blank" rel="noopener noreferrer">
27
+ {children}
28
+ </a>
29
+ ),
30
+ blockquote: ({ children }) => <blockquote className="border-l-4 border-border pl-4 italic my-4">{children}</blockquote>,
31
+ table: ({ children }) => <table className="w-full border-collapse my-4 border-slate-300 dark:border-slate-200">{children}</table>,
32
+ thead: ({ children }) => <thead className="bg-muted/50">{children}</thead>,
33
+ };
34
+
35
+ interface MessageProps {
36
+ message: MessageType;
37
+ }
38
+
39
+ const Message: React.FC<MessageProps> = ({ message }) => {
40
+ const isUser = message.role === "user";
41
+ const isProgress = message.content.includes("%)") && message.role === "assistant";
42
+
43
+ const copyToClipboard = () => {
44
+ navigator.clipboard.writeText(message.content);
45
+ };
46
+
47
+ return (
48
+ <div className="py-2 px-4">
49
+ <div className={`max-w-2xl mx-auto flex gap-4 relative ${isUser ? "flex-row-reverse" : ""}`}>
50
+ <Avatar className={`h-8 w-8 shrink-0 absolute justify-center item-center ${isUser ? "-right-12" : "-left-12"} top-0`}>{isUser ? <User2 className="h-5 w-5" /> : <Bot className="h-5 w-5" />}</Avatar>
51
+
52
+ <div className={`flex-1 ${isUser ? "items-end" : "items-start"}`}>
53
+ <div className={`flex items-center gap-2 mb-1 ${isUser ? "justify-end" : "justify-start"}`}>
54
+ <div className="text-xs text-muted-foreground">{new Date(message.timestamp).toLocaleTimeString()}</div>
55
+ <div className="font-medium">{isUser ? "You" : "Research Assistant"}</div>
56
+
57
+ {!isUser && (
58
+ <div className="ml-auto flex items-center gap-2">
59
+ <TooltipProvider>
60
+ <Tooltip>
61
+ <TooltipTrigger asChild>
62
+ <Button size="icon" variant="ghost" className="h-8 w-8" onClick={copyToClipboard}>
63
+ <Copy className="h-4 w-4" />
64
+ </Button>
65
+ </TooltipTrigger>
66
+ <TooltipContent>Copy to clipboard</TooltipContent>
67
+ </Tooltip>
68
+ </TooltipProvider>
69
+
70
+ <DropdownMenu>
71
+ <DropdownMenuTrigger asChild>
72
+ <Button size="icon" variant="ghost" className="h-8 w-8">
73
+ <MoreHorizontal className="h-4 w-4" />
74
+ </Button>
75
+ </DropdownMenuTrigger>
76
+ <DropdownMenuContent align="end">
77
+ <DropdownMenuItem onClick={copyToClipboard}>Copy text</DropdownMenuItem>
78
+ <DropdownMenuItem>Show sources</DropdownMenuItem>
79
+ <DropdownMenuItem>View in visualizations</DropdownMenuItem>
80
+ </DropdownMenuContent>
81
+ </DropdownMenu>
82
+ </div>
83
+ )}
84
+ </div>
85
+
86
+ <div className={`mt-1 max-w-none ${isUser ? "bg-slate-300 dark:bg-slate-200 dark:text-background text-foreground" : isProgress ? "bg-muted/30" : "bg-muted/50"} p-3 rounded-2xl ${isUser ? "rounded-tr-sm" : "rounded-tl-sm"}`} style={{ overflowWrap: "anywhere" }}>
87
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={MarkdownComponents}>
88
+ {message.content}
89
+ </ReactMarkdown>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ );
95
+ };
96
+
97
+ export default Message;
frontend/src/components/MessageInput.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { Button } from "@/components/ui/button";
3
+ import { AutosizeTextarea } from "@/components/ui/textarea";
4
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
5
+ import { Mic, Paperclip, Send } from "lucide-react";
6
+ import React, { useState } from "react";
7
+
8
+ interface MessageInputProps {
9
+ onSendMessage: (content: string) => void;
10
+ isLoading: boolean;
11
+ }
12
+
13
+ const MessageInput: React.FC<MessageInputProps> = ({ onSendMessage, isLoading }) => {
14
+ const [message, setMessage] = useState("");
15
+
16
+ const handleMessageChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
17
+ setMessage(e.target.value);
18
+ };
19
+
20
+ const handleSubmit = (e: React.FormEvent) => {
21
+ e.preventDefault();
22
+ if (message.trim() && !isLoading) {
23
+ onSendMessage(message);
24
+ setMessage("");
25
+ }
26
+ };
27
+
28
+ const handleKeyDown = (e: React.KeyboardEvent) => {
29
+ if (e.key === "Enter" && !e.shiftKey) {
30
+ e.preventDefault();
31
+ handleSubmit(e);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="p-4 absolute bottom-0 left-0 right-0 bg-transparent mb-4">
37
+ <form onSubmit={handleSubmit} className="max-w-4xl mx-auto">
38
+ <div className="relative flex items-center bg-background shadow-lg rounded-[2rem] border overflow-hidden h-full">
39
+ <AutosizeTextarea placeholder="Ask a research question..." maxHeight={500} minHeight={52} className="pr-36 pl-6 py-4 font-medium border-none h-auto resize-none" value={message} onChange={handleMessageChange} onKeyDown={handleKeyDown} disabled={isLoading} rows={1} autoFocus />
40
+
41
+ <div className="absolute right-3 flex items-center gap-2 h-full">
42
+ <TooltipProvider>
43
+ <Tooltip>
44
+ <TooltipTrigger asChild>
45
+ <Button type="button" size="icon" variant="ghost" className="h-9 w-9" disabled={isLoading}>
46
+ <Paperclip className="h-4 w-4" />
47
+ </Button>
48
+ </TooltipTrigger>
49
+ <TooltipContent>Attach files (coming soon)</TooltipContent>
50
+ </Tooltip>
51
+ </TooltipProvider>
52
+
53
+ <TooltipProvider>
54
+ <Tooltip>
55
+ <TooltipTrigger asChild>
56
+ <Button type="button" size="icon" variant="ghost" className="h-9 w-9" disabled={isLoading}>
57
+ <Mic className="h-4 w-4" />
58
+ </Button>
59
+ </TooltipTrigger>
60
+ <TooltipContent>Voice input (coming soon)</TooltipContent>
61
+ </Tooltip>
62
+ </TooltipProvider>
63
+
64
+ <Button type="submit" size="icon" className="h-9 w-9 rounded-full" disabled={isLoading || !message.trim()}>
65
+ <Send className="h-4 w-4" />
66
+ <span className="sr-only">Send message</span>
67
+ </Button>
68
+ </div>
69
+ </div>
70
+ </form>
71
+ </div>
72
+ );
73
+ };
74
+
75
+ export default MessageInput;
frontend/src/components/ResearchControls.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Checkbox } from "@/components/ui/checkbox";
2
+ import { Label } from "@/components/ui/label";
3
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
4
+ import { Separator } from "@/components/ui/separator";
5
+ import { ResearchOptions } from "@/lib/types";
6
+ import React from "react";
7
+
8
+ interface ResearchControlsProps {
9
+ options: ResearchOptions;
10
+ onOptionChange: (options: ResearchOptions) => void;
11
+ }
12
+
13
+ const ResearchControls: React.FC<ResearchControlsProps> = ({ options, onOptionChange }) => {
14
+ return (
15
+ <div className="space-y-6">
16
+ <div className="space-y-3">
17
+ <Label htmlFor="research-depth">Research Depth</Label>
18
+ <Select value={options.depth} onValueChange={(value: ResearchOptions["depth"]) => onOptionChange({ ...options, depth: value })}>
19
+ <SelectTrigger id="research-depth" className="w-full">
20
+ <SelectValue placeholder="Select depth" />
21
+ </SelectTrigger>
22
+ <SelectContent>
23
+ <SelectItem value="basic">Basic</SelectItem>
24
+ <SelectItem value="intermediate">Intermediate</SelectItem>
25
+ <SelectItem value="deep">Deep</SelectItem>
26
+ </SelectContent>
27
+ </Select>
28
+ <p className="text-xs text-muted-foreground">Determines how extensively the assistant will research your query.</p>
29
+ </div>
30
+
31
+ <Separator />
32
+
33
+ <div className="space-y-3">
34
+ <Label>Inclusions</Label>
35
+
36
+ <div className="flex items-center space-x-2">
37
+ <Checkbox id="sources" checked={options.sources} onCheckedChange={(checked) => onOptionChange({ ...options, sources: checked as boolean })} />
38
+ <Label htmlFor="sources" className="text-sm font-normal">
39
+ Include sources
40
+ </Label>
41
+ </div>
42
+
43
+ <div className="flex items-center space-x-2">
44
+ <Checkbox id="citations" checked={options.citations} onCheckedChange={(checked) => onOptionChange({ ...options, citations: checked as boolean })} />
45
+ <Label htmlFor="citations" className="text-sm font-normal">
46
+ Include citations
47
+ </Label>
48
+ </div>
49
+ </div>
50
+
51
+ <Separator />
52
+
53
+ <div className="space-y-3">
54
+ <Label>Coming Soon</Label>
55
+ <div className="flex items-center space-x-2">
56
+ <Checkbox id="visualize" disabled />
57
+ <Label htmlFor="visualize" className="text-sm font-normal text-muted-foreground">
58
+ Generate visualizations
59
+ </Label>
60
+ </div>
61
+
62
+ <div className="flex items-center space-x-2">
63
+ <Checkbox id="video-content" disabled />
64
+ <Label htmlFor="video-content" className="text-sm font-normal text-muted-foreground">
65
+ Include video content
66
+ </Label>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ );
71
+ };
72
+
73
+ export default ResearchControls;
frontend/src/components/theme-provider.tsx ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
4
+ import * as React from "react";
5
+
6
+ export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
7
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
8
+ }
frontend/src/components/ui/ChatLayout.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { Card } from "@/components/ui/card";
3
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
4
+ import { ScrollArea } from "@/components/ui/scroll-area";
5
+ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
6
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
7
+ import { LayoutGrid, MessageCircle, Settings } from "lucide-react";
8
+ import React from "react";
9
+ import { ThemeToggle } from "./ThemeToggle";
10
+
11
+ interface ChatLayoutProps {
12
+ sidebar: React.ReactNode;
13
+ mainContent: React.ReactNode;
14
+ settingsPanel: React.ReactNode;
15
+ }
16
+
17
+ const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsPanel }) => {
18
+ return (
19
+ <div className="h-screen flex flex-col">
20
+ <header className="border-b-2 h-14 flex items-center px-6">
21
+ <h1 className="text-xl font-semibold">Deep Research Assistant</h1>
22
+ <div className="flex-1" />
23
+ <ThemeToggle />
24
+ <Sheet>
25
+ <SheetTrigger asChild>
26
+ <Button variant="ghost" size="icon">
27
+ <Settings size={20} />
28
+ </Button>
29
+ </SheetTrigger>
30
+ <SheetContent>
31
+ <div className="py-4">
32
+ <h2 className="text-lg font-semibold mb-4">Research Settings</h2>
33
+ {settingsPanel}
34
+ </div>
35
+ </SheetContent>
36
+ </Sheet>
37
+ </header>
38
+
39
+ <div className="flex-1 overflow-hidden">
40
+ <ResizablePanelGroup direction="horizontal">
41
+ <ResizablePanel defaultSize={20} minSize={15} maxSize={30} className="hidden md:block">
42
+ <Card className="h-full rounded-none border-r border-t-0 border-l-0 border-b-0">
43
+ <ScrollArea className="h-full">{sidebar}</ScrollArea>
44
+ </Card>
45
+ </ResizablePanel>
46
+
47
+ <ResizableHandle withHandle className="hidden md:flex" />
48
+
49
+ <ResizablePanel defaultSize={80}>
50
+ <Tabs defaultValue="chat" className="h-full flex flex-col">
51
+ <div className="p-4">
52
+ <TabsList className="">
53
+ <TabsTrigger value="chat" className="flex items-center gap-2">
54
+ <MessageCircle size={16} />
55
+ <span>Chat</span>
56
+ </TabsTrigger>
57
+ <TabsTrigger value="visualizations" className="flex items-center gap-2">
58
+ <LayoutGrid size={16} />
59
+ <span>Visualizations</span>
60
+ </TabsTrigger>
61
+ </TabsList>
62
+ </div>
63
+
64
+ <TabsContent value="chat" className="overflow-auto flex flex-1" tabIndex={-1}>
65
+ {mainContent}
66
+ </TabsContent>
67
+
68
+ <TabsContent value="visualizations" className="p-4 flex-1">
69
+ <div className="h-full flex items-center justify-center text-muted-foreground">
70
+ <div className="text-center">
71
+ <LayoutGrid className="mx-auto h-12 w-12 mb-4 opacity-30" />
72
+ <h3 className="text-lg font-medium mb-2">Visualizations Coming Soon</h3>
73
+ <p>View graphs, charts, and other visual representations of your research data.</p>
74
+ </div>
75
+ </div>
76
+ </TabsContent>
77
+ </Tabs>
78
+ </ResizablePanel>
79
+ </ResizablePanelGroup>
80
+ </div>
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default ChatLayout;
frontend/src/components/ui/ConversationList.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { Button } from "@/components/ui/button";
3
+ import { MessageSquare, PlusCircle } from "lucide-react";
4
+ import React from "react";
5
+
6
+ interface Conversation {
7
+ id: string;
8
+ title: string;
9
+ lastUpdated: string;
10
+ active?: boolean;
11
+ }
12
+
13
+ interface ConversationListProps {
14
+ conversations: Conversation[];
15
+ onNewConversation: () => void;
16
+ onSelectConversation: (id: string) => void;
17
+ }
18
+
19
+ const ConversationList: React.FC<ConversationListProps> = ({ conversations, onNewConversation, onSelectConversation }) => {
20
+ const handleSelectConversation = (id: string) => {
21
+ onSelectConversation(id);
22
+ };
23
+
24
+ return (
25
+ <div className="flex flex-col h-full py-2">
26
+ <div className="px-4 py-2">
27
+ <Button onClick={onNewConversation} className="w-full justify-start" variant="outline">
28
+ <PlusCircle className="mr-2 h-4 w-4" />
29
+ New Research
30
+ </Button>
31
+ </div>
32
+
33
+ <div className="px-2 py-2">
34
+ <h2 className="text-sm font-semibold px-2 mb-2">Recent Research</h2>
35
+ <div className="space-y-1">
36
+ {conversations.length === 0 ? (
37
+ <p className="text-sm text-muted-foreground px-2">No conversations yet</p>
38
+ ) : (
39
+ conversations.map((conversation) => (
40
+ <Button key={conversation.id} variant={conversation.active ? "secondary" : "ghost"} className={`w-full justify-start text-left truncate ${conversation.active ? "bg-accent" : ""}`} onClick={() => handleSelectConversation(conversation.id)}>
41
+ <MessageSquare className="mr-2 h-4 w-4 shrink-0" />
42
+ <span className="truncate">{conversation.title}</span>
43
+ </Button>
44
+ ))
45
+ )}
46
+ </div>
47
+ </div>
48
+ </div>
49
+ );
50
+ };
51
+
52
+ export default ConversationList;
frontend/src/components/ui/ThemeToggle.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button } from "@/components/ui/button";
2
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
3
+ import { Laptop, Moon, Sun } from "lucide-react";
4
+ import { useTheme } from "next-themes";
5
+
6
+ export function ThemeToggle() {
7
+ const { setTheme } = useTheme();
8
+
9
+ return (
10
+ <DropdownMenu>
11
+ <DropdownMenuTrigger asChild>
12
+ <Button variant="ghost" size="icon">
13
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 transition-all dark:-rotate-90 dark:hidden" />
14
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] hidden rotate-90 transition-all dark:rotate-0 dark:block" />
15
+ <span className="sr-only">Toggle theme</span>
16
+ </Button>
17
+ </DropdownMenuTrigger>
18
+ <DropdownMenuContent align="end">
19
+ <DropdownMenuItem onClick={() => setTheme("light")}>
20
+ <Sun className="mr-2 h-4 w-4" />
21
+ <span>Light</span>
22
+ </DropdownMenuItem>
23
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
24
+ <Moon className="mr-2 h-4 w-4" />
25
+ <span>Dark</span>
26
+ </DropdownMenuItem>
27
+ <DropdownMenuItem onClick={() => setTheme("system")}>
28
+ <Laptop className="mr-2 h-4 w-4" />
29
+ <span>System</span>
30
+ </DropdownMenuItem>
31
+ </DropdownMenuContent>
32
+ </DropdownMenu>
33
+ );
34
+ }
frontend/src/components/ui/alert.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import * as React from "react";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const alertVariants = cva("relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", {
7
+ variants: {
8
+ variant: {
9
+ default: "bg-background text-foreground",
10
+ destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
11
+ },
12
+ },
13
+ defaultVariants: {
14
+ variant: "default",
15
+ },
16
+ });
17
+
18
+ const Alert = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>>(({ className, variant, ...props }, ref) => <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />);
19
+ Alert.displayName = "Alert";
20
+
21
+ const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => <h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />);
22
+ AlertTitle.displayName = "AlertTitle";
23
+
24
+ const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />);
25
+ AlertDescription.displayName = "AlertDescription";
26
+
27
+ export { Alert, AlertDescription, AlertTitle };
frontend/src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Avatar = React.forwardRef<
9
+ React.ElementRef<typeof AvatarPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <AvatarPrimitive.Root
13
+ ref={ref}
14
+ className={cn(
15
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ ))
21
+ Avatar.displayName = AvatarPrimitive.Root.displayName
22
+
23
+ const AvatarImage = React.forwardRef<
24
+ React.ElementRef<typeof AvatarPrimitive.Image>,
25
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
26
+ >(({ className, ...props }, ref) => (
27
+ <AvatarPrimitive.Image
28
+ ref={ref}
29
+ className={cn("aspect-square h-full w-full", className)}
30
+ {...props}
31
+ />
32
+ ))
33
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName
34
+
35
+ const AvatarFallback = React.forwardRef<
36
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
37
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
38
+ >(({ className, ...props }, ref) => (
39
+ <AvatarPrimitive.Fallback
40
+ ref={ref}
41
+ className={cn(
42
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ ))
48
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49
+
50
+ export { Avatar, AvatarImage, AvatarFallback }
frontend/src/components/ui/button.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16
+ outline:
17
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20
+ ghost: "hover:bg-accent hover:text-accent-foreground",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2",
25
+ sm: "h-8 rounded-md px-3 text-xs",
26
+ lg: "h-10 rounded-md px-8",
27
+ icon: "h-9 w-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ export interface ButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean
41
+ }
42
+
43
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : "button"
46
+ return (
47
+ <Comp
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ ref={ref}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+ )
55
+ Button.displayName = "Button"
56
+
57
+ export { Button, buttonVariants }
frontend/src/components/ui/card.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-xl border bg-card text-card-foreground shadow",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLDivElement,
34
+ React.HTMLAttributes<HTMLDivElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <div
37
+ ref={ref}
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ ))
42
+ CardTitle.displayName = "CardTitle"
43
+
44
+ const CardDescription = React.forwardRef<
45
+ HTMLDivElement,
46
+ React.HTMLAttributes<HTMLDivElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <div
49
+ ref={ref}
50
+ className={cn("text-sm text-muted-foreground", className)}
51
+ {...props}
52
+ />
53
+ ))
54
+ CardDescription.displayName = "CardDescription"
55
+
56
+ const CardContent = React.forwardRef<
57
+ HTMLDivElement,
58
+ React.HTMLAttributes<HTMLDivElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61
+ ))
62
+ CardContent.displayName = "CardContent"
63
+
64
+ const CardFooter = React.forwardRef<
65
+ HTMLDivElement,
66
+ React.HTMLAttributes<HTMLDivElement>
67
+ >(({ className, ...props }, ref) => (
68
+ <div
69
+ ref={ref}
70
+ className={cn("flex items-center p-6 pt-0", className)}
71
+ {...props}
72
+ />
73
+ ))
74
+ CardFooter.displayName = "CardFooter"
75
+
76
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
frontend/src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5
+ import { Check } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Checkbox = React.forwardRef<
10
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
11
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
12
+ >(({ className, ...props }, ref) => (
13
+ <CheckboxPrimitive.Root
14
+ ref={ref}
15
+ className={cn(
16
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
17
+ className
18
+ )}
19
+ {...props}
20
+ >
21
+ <CheckboxPrimitive.Indicator
22
+ className={cn("flex items-center justify-center text-current")}
23
+ >
24
+ <Check className="h-4 w-4" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ ))
28
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
29
+
30
+ export { Checkbox }
frontend/src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
5
+ import { X } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = DialogPrimitive.Portal
14
+
15
+ const DialogClose = DialogPrimitive.Close
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ ))
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
+ <X className="h-4 w-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ))
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName
55
+
56
+ const DialogHeader = ({
57
+ className,
58
+ ...props
59
+ }: React.HTMLAttributes<HTMLDivElement>) => (
60
+ <div
61
+ className={cn(
62
+ "flex flex-col space-y-1.5 text-center sm:text-left",
63
+ className
64
+ )}
65
+ {...props}
66
+ />
67
+ )
68
+ DialogHeader.displayName = "DialogHeader"
69
+
70
+ const DialogFooter = ({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) => (
74
+ <div
75
+ className={cn(
76
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ )
82
+ DialogFooter.displayName = "DialogFooter"
83
+
84
+ const DialogTitle = React.forwardRef<
85
+ React.ElementRef<typeof DialogPrimitive.Title>,
86
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87
+ >(({ className, ...props }, ref) => (
88
+ <DialogPrimitive.Title
89
+ ref={ref}
90
+ className={cn(
91
+ "text-lg font-semibold leading-none tracking-tight",
92
+ className
93
+ )}
94
+ {...props}
95
+ />
96
+ ))
97
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
98
+
99
+ const DialogDescription = React.forwardRef<
100
+ React.ElementRef<typeof DialogPrimitive.Description>,
101
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102
+ >(({ className, ...props }, ref) => (
103
+ <DialogPrimitive.Description
104
+ ref={ref}
105
+ className={cn("text-sm text-muted-foreground", className)}
106
+ {...props}
107
+ />
108
+ ))
109
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
110
+
111
+ export {
112
+ Dialog,
113
+ DialogPortal,
114
+ DialogOverlay,
115
+ DialogTrigger,
116
+ DialogClose,
117
+ DialogContent,
118
+ DialogHeader,
119
+ DialogFooter,
120
+ DialogTitle,
121
+ DialogDescription,
122
+ }
frontend/src/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
+ import { Check, ChevronRight, Circle } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const DropdownMenu = DropdownMenuPrimitive.Root
10
+
11
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
+
13
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
14
+
15
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16
+
17
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
18
+
19
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20
+
21
+ const DropdownMenuSubTrigger = React.forwardRef<
22
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
+ inset?: boolean
25
+ }
26
+ >(({ className, inset, children, ...props }, ref) => (
27
+ <DropdownMenuPrimitive.SubTrigger
28
+ ref={ref}
29
+ className={cn(
30
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
31
+ inset && "pl-8",
32
+ className
33
+ )}
34
+ {...props}
35
+ >
36
+ {children}
37
+ <ChevronRight className="ml-auto h-4 w-4" />
38
+ </DropdownMenuPrimitive.SubTrigger>
39
+ ))
40
+ DropdownMenuSubTrigger.displayName =
41
+ DropdownMenuPrimitive.SubTrigger.displayName
42
+
43
+ const DropdownMenuSubContent = React.forwardRef<
44
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
45
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
46
+ >(({ className, ...props }, ref) => (
47
+ <DropdownMenuPrimitive.SubContent
48
+ ref={ref}
49
+ className={cn(
50
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
51
+ className
52
+ )}
53
+ {...props}
54
+ />
55
+ ))
56
+ DropdownMenuSubContent.displayName =
57
+ DropdownMenuPrimitive.SubContent.displayName
58
+
59
+ const DropdownMenuContent = React.forwardRef<
60
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62
+ >(({ className, sideOffset = 4, ...props }, ref) => (
63
+ <DropdownMenuPrimitive.Portal>
64
+ <DropdownMenuPrimitive.Content
65
+ ref={ref}
66
+ sideOffset={sideOffset}
67
+ className={cn(
68
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
69
+ className
70
+ )}
71
+ {...props}
72
+ />
73
+ </DropdownMenuPrimitive.Portal>
74
+ ))
75
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76
+
77
+ const DropdownMenuItem = React.forwardRef<
78
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
79
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
80
+ inset?: boolean
81
+ }
82
+ >(({ className, inset, ...props }, ref) => (
83
+ <DropdownMenuPrimitive.Item
84
+ ref={ref}
85
+ className={cn(
86
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
87
+ inset && "pl-8",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ ))
93
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94
+
95
+ const DropdownMenuCheckboxItem = React.forwardRef<
96
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
97
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
98
+ >(({ className, children, checked, ...props }, ref) => (
99
+ <DropdownMenuPrimitive.CheckboxItem
100
+ ref={ref}
101
+ className={cn(
102
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
103
+ className
104
+ )}
105
+ checked={checked}
106
+ {...props}
107
+ >
108
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
109
+ <DropdownMenuPrimitive.ItemIndicator>
110
+ <Check className="h-4 w-4" />
111
+ </DropdownMenuPrimitive.ItemIndicator>
112
+ </span>
113
+ {children}
114
+ </DropdownMenuPrimitive.CheckboxItem>
115
+ ))
116
+ DropdownMenuCheckboxItem.displayName =
117
+ DropdownMenuPrimitive.CheckboxItem.displayName
118
+
119
+ const DropdownMenuRadioItem = React.forwardRef<
120
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
121
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
122
+ >(({ className, children, ...props }, ref) => (
123
+ <DropdownMenuPrimitive.RadioItem
124
+ ref={ref}
125
+ className={cn(
126
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
127
+ className
128
+ )}
129
+ {...props}
130
+ >
131
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
132
+ <DropdownMenuPrimitive.ItemIndicator>
133
+ <Circle className="h-2 w-2 fill-current" />
134
+ </DropdownMenuPrimitive.ItemIndicator>
135
+ </span>
136
+ {children}
137
+ </DropdownMenuPrimitive.RadioItem>
138
+ ))
139
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140
+
141
+ const DropdownMenuLabel = React.forwardRef<
142
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
143
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
144
+ inset?: boolean
145
+ }
146
+ >(({ className, inset, ...props }, ref) => (
147
+ <DropdownMenuPrimitive.Label
148
+ ref={ref}
149
+ className={cn(
150
+ "px-2 py-1.5 text-sm font-semibold",
151
+ inset && "pl-8",
152
+ className
153
+ )}
154
+ {...props}
155
+ />
156
+ ))
157
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158
+
159
+ const DropdownMenuSeparator = React.forwardRef<
160
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
161
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
162
+ >(({ className, ...props }, ref) => (
163
+ <DropdownMenuPrimitive.Separator
164
+ ref={ref}
165
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
166
+ {...props}
167
+ />
168
+ ))
169
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170
+
171
+ const DropdownMenuShortcut = ({
172
+ className,
173
+ ...props
174
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
175
+ return (
176
+ <span
177
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
178
+ {...props}
179
+ />
180
+ )
181
+ }
182
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183
+
184
+ export {
185
+ DropdownMenu,
186
+ DropdownMenuTrigger,
187
+ DropdownMenuContent,
188
+ DropdownMenuItem,
189
+ DropdownMenuCheckboxItem,
190
+ DropdownMenuRadioItem,
191
+ DropdownMenuLabel,
192
+ DropdownMenuSeparator,
193
+ DropdownMenuShortcut,
194
+ DropdownMenuGroup,
195
+ DropdownMenuPortal,
196
+ DropdownMenuSub,
197
+ DropdownMenuSubContent,
198
+ DropdownMenuSubTrigger,
199
+ DropdownMenuRadioGroup,
200
+ }
frontend/src/components/ui/input.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
+ ({ className, type, ...props }, ref) => {
7
+ return (
8
+ <input
9
+ type={type}
10
+ className={cn(
11
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ className
13
+ )}
14
+ ref={ref}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+ )
20
+ Input.displayName = "Input"
21
+
22
+ export { Input }
frontend/src/components/ui/label.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const labelVariants = cva(
10
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11
+ )
12
+
13
+ const Label = React.forwardRef<
14
+ React.ElementRef<typeof LabelPrimitive.Root>,
15
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
16
+ VariantProps<typeof labelVariants>
17
+ >(({ className, ...props }, ref) => (
18
+ <LabelPrimitive.Root
19
+ ref={ref}
20
+ className={cn(labelVariants(), className)}
21
+ {...props}
22
+ />
23
+ ))
24
+ Label.displayName = LabelPrimitive.Root.displayName
25
+
26
+ export { Label }
frontend/src/components/ui/resizable.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { GripVertical } from "lucide-react"
4
+ import * as ResizablePrimitive from "react-resizable-panels"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const ResizablePanelGroup = ({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
12
+ <ResizablePrimitive.PanelGroup
13
+ className={cn(
14
+ "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+
21
+ const ResizablePanel = ResizablePrimitive.Panel
22
+
23
+ const ResizableHandle = ({
24
+ withHandle,
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
28
+ withHandle?: boolean
29
+ }) => (
30
+ <ResizablePrimitive.PanelResizeHandle
31
+ className={cn(
32
+ "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
33
+ className
34
+ )}
35
+ {...props}
36
+ >
37
+ {withHandle && (
38
+ <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
39
+ <GripVertical className="h-2.5 w-2.5" />
40
+ </div>
41
+ )}
42
+ </ResizablePrimitive.PanelResizeHandle>
43
+ )
44
+
45
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
frontend/src/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const ScrollArea = React.forwardRef<React.ElementRef<typeof ScrollAreaPrimitive.Root>, React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>>(({ className, children, ...props }, ref) => (
9
+ <ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
10
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
11
+ <ScrollBar />
12
+ <ScrollAreaPrimitive.Corner />
13
+ </ScrollAreaPrimitive.Root>
14
+ ));
15
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
16
+
17
+ const ScrollBar = React.forwardRef<React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>>(({ className, orientation = "vertical", ...props }, ref) => (
18
+ <ScrollAreaPrimitive.ScrollAreaScrollbar ref={ref} orientation={orientation} className={cn("flex touch-none select-none transition-colors", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]", className)} {...props}>
19
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
20
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
21
+ ));
22
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
23
+
24
+ export { ScrollArea, ScrollBar };
frontend/src/components/ui/select.tsx ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SelectPrimitive from "@radix-ui/react-select"
5
+ import { Check, ChevronDown, ChevronUp } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const Select = SelectPrimitive.Root
10
+
11
+ const SelectGroup = SelectPrimitive.Group
12
+
13
+ const SelectValue = SelectPrimitive.Value
14
+
15
+ const SelectTrigger = React.forwardRef<
16
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
17
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18
+ >(({ className, children, ...props }, ref) => (
19
+ <SelectPrimitive.Trigger
20
+ ref={ref}
21
+ className={cn(
22
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
23
+ className
24
+ )}
25
+ {...props}
26
+ >
27
+ {children}
28
+ <SelectPrimitive.Icon asChild>
29
+ <ChevronDown className="h-4 w-4 opacity-50" />
30
+ </SelectPrimitive.Icon>
31
+ </SelectPrimitive.Trigger>
32
+ ))
33
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34
+
35
+ const SelectScrollUpButton = React.forwardRef<
36
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
37
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
38
+ >(({ className, ...props }, ref) => (
39
+ <SelectPrimitive.ScrollUpButton
40
+ ref={ref}
41
+ className={cn(
42
+ "flex cursor-default items-center justify-center py-1",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <ChevronUp className="h-4 w-4" />
48
+ </SelectPrimitive.ScrollUpButton>
49
+ ))
50
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51
+
52
+ const SelectScrollDownButton = React.forwardRef<
53
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
54
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
55
+ >(({ className, ...props }, ref) => (
56
+ <SelectPrimitive.ScrollDownButton
57
+ ref={ref}
58
+ className={cn(
59
+ "flex cursor-default items-center justify-center py-1",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ <ChevronDown className="h-4 w-4" />
65
+ </SelectPrimitive.ScrollDownButton>
66
+ ))
67
+ SelectScrollDownButton.displayName =
68
+ SelectPrimitive.ScrollDownButton.displayName
69
+
70
+ const SelectContent = React.forwardRef<
71
+ React.ElementRef<typeof SelectPrimitive.Content>,
72
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
73
+ >(({ className, children, position = "popper", ...props }, ref) => (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Content
76
+ ref={ref}
77
+ className={cn(
78
+ "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
79
+ position === "popper" &&
80
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
81
+ className
82
+ )}
83
+ position={position}
84
+ {...props}
85
+ >
86
+ <SelectScrollUpButton />
87
+ <SelectPrimitive.Viewport
88
+ className={cn(
89
+ "p-1",
90
+ position === "popper" &&
91
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
92
+ )}
93
+ >
94
+ {children}
95
+ </SelectPrimitive.Viewport>
96
+ <SelectScrollDownButton />
97
+ </SelectPrimitive.Content>
98
+ </SelectPrimitive.Portal>
99
+ ))
100
+ SelectContent.displayName = SelectPrimitive.Content.displayName
101
+
102
+ const SelectLabel = React.forwardRef<
103
+ React.ElementRef<typeof SelectPrimitive.Label>,
104
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
105
+ >(({ className, ...props }, ref) => (
106
+ <SelectPrimitive.Label
107
+ ref={ref}
108
+ className={cn("px-2 py-1.5 text-sm font-semibold", className)}
109
+ {...props}
110
+ />
111
+ ))
112
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
113
+
114
+ const SelectItem = React.forwardRef<
115
+ React.ElementRef<typeof SelectPrimitive.Item>,
116
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
117
+ >(({ className, children, ...props }, ref) => (
118
+ <SelectPrimitive.Item
119
+ ref={ref}
120
+ className={cn(
121
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
122
+ className
123
+ )}
124
+ {...props}
125
+ >
126
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
127
+ <SelectPrimitive.ItemIndicator>
128
+ <Check className="h-4 w-4" />
129
+ </SelectPrimitive.ItemIndicator>
130
+ </span>
131
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
132
+ </SelectPrimitive.Item>
133
+ ))
134
+ SelectItem.displayName = SelectPrimitive.Item.displayName
135
+
136
+ const SelectSeparator = React.forwardRef<
137
+ React.ElementRef<typeof SelectPrimitive.Separator>,
138
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
139
+ >(({ className, ...props }, ref) => (
140
+ <SelectPrimitive.Separator
141
+ ref={ref}
142
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
143
+ {...props}
144
+ />
145
+ ))
146
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
147
+
148
+ export {
149
+ Select,
150
+ SelectGroup,
151
+ SelectValue,
152
+ SelectTrigger,
153
+ SelectContent,
154
+ SelectLabel,
155
+ SelectItem,
156
+ SelectSeparator,
157
+ SelectScrollUpButton,
158
+ SelectScrollDownButton,
159
+ }
frontend/src/components/ui/separator.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SeparatorPrimitive from "@radix-ui/react-separator"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Separator = React.forwardRef<
9
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
+ >(
12
+ (
13
+ { className, orientation = "horizontal", decorative = true, ...props },
14
+ ref
15
+ ) => (
16
+ <SeparatorPrimitive.Root
17
+ ref={ref}
18
+ decorative={decorative}
19
+ orientation={orientation}
20
+ className={cn(
21
+ "shrink-0 bg-border",
22
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ )
29
+ Separator.displayName = SeparatorPrimitive.Root.displayName
30
+
31
+ export { Separator }
frontend/src/components/ui/sheet.tsx ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SheetPrimitive from "@radix-ui/react-dialog"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+ import { X } from "lucide-react"
7
+
8
+ import { cn } from "@/lib/utils"
9
+
10
+ const Sheet = SheetPrimitive.Root
11
+
12
+ const SheetTrigger = SheetPrimitive.Trigger
13
+
14
+ const SheetClose = SheetPrimitive.Close
15
+
16
+ const SheetPortal = SheetPrimitive.Portal
17
+
18
+ const SheetOverlay = React.forwardRef<
19
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <SheetPrimitive.Overlay
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
+ className
26
+ )}
27
+ {...props}
28
+ ref={ref}
29
+ />
30
+ ))
31
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32
+
33
+ const sheetVariants = cva(
34
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35
+ {
36
+ variants: {
37
+ side: {
38
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39
+ bottom:
40
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42
+ right:
43
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ side: "right",
48
+ },
49
+ }
50
+ )
51
+
52
+ interface SheetContentProps
53
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
54
+ VariantProps<typeof sheetVariants> {}
55
+
56
+ const SheetContent = React.forwardRef<
57
+ React.ElementRef<typeof SheetPrimitive.Content>,
58
+ SheetContentProps
59
+ >(({ side = "right", className, children, ...props }, ref) => (
60
+ <SheetPortal>
61
+ <SheetOverlay />
62
+ <SheetPrimitive.Content
63
+ ref={ref}
64
+ className={cn(sheetVariants({ side }), className)}
65
+ {...props}
66
+ >
67
+ <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
68
+ <X className="h-4 w-4" />
69
+ <span className="sr-only">Close</span>
70
+ </SheetPrimitive.Close>
71
+ {children}
72
+ </SheetPrimitive.Content>
73
+ </SheetPortal>
74
+ ))
75
+ SheetContent.displayName = SheetPrimitive.Content.displayName
76
+
77
+ const SheetHeader = ({
78
+ className,
79
+ ...props
80
+ }: React.HTMLAttributes<HTMLDivElement>) => (
81
+ <div
82
+ className={cn(
83
+ "flex flex-col space-y-2 text-center sm:text-left",
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ )
89
+ SheetHeader.displayName = "SheetHeader"
90
+
91
+ const SheetFooter = ({
92
+ className,
93
+ ...props
94
+ }: React.HTMLAttributes<HTMLDivElement>) => (
95
+ <div
96
+ className={cn(
97
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
98
+ className
99
+ )}
100
+ {...props}
101
+ />
102
+ )
103
+ SheetFooter.displayName = "SheetFooter"
104
+
105
+ const SheetTitle = React.forwardRef<
106
+ React.ElementRef<typeof SheetPrimitive.Title>,
107
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
108
+ >(({ className, ...props }, ref) => (
109
+ <SheetPrimitive.Title
110
+ ref={ref}
111
+ className={cn("text-lg font-semibold text-foreground", className)}
112
+ {...props}
113
+ />
114
+ ))
115
+ SheetTitle.displayName = SheetPrimitive.Title.displayName
116
+
117
+ const SheetDescription = React.forwardRef<
118
+ React.ElementRef<typeof SheetPrimitive.Description>,
119
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
120
+ >(({ className, ...props }, ref) => (
121
+ <SheetPrimitive.Description
122
+ ref={ref}
123
+ className={cn("text-sm text-muted-foreground", className)}
124
+ {...props}
125
+ />
126
+ ))
127
+ SheetDescription.displayName = SheetPrimitive.Description.displayName
128
+
129
+ export {
130
+ Sheet,
131
+ SheetPortal,
132
+ SheetOverlay,
133
+ SheetTrigger,
134
+ SheetClose,
135
+ SheetContent,
136
+ SheetHeader,
137
+ SheetFooter,
138
+ SheetTitle,
139
+ SheetDescription,
140
+ }
frontend/src/components/ui/tabs.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const Tabs = TabsPrimitive.Root;
9
+
10
+ const TabsList = React.forwardRef<React.ElementRef<typeof TabsPrimitive.List>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>>(({ className, ...props }, ref) => <TabsPrimitive.List ref={ref} className={cn("inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground", className)} {...props} />);
11
+ TabsList.displayName = TabsPrimitive.List.displayName;
12
+
13
+ const TabsTrigger = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>>(({ className, ...props }, ref) => (
14
+ <TabsPrimitive.Trigger ref={ref} className={cn("inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow", className)} {...props} />
15
+ ));
16
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
17
+
18
+ const TabsContent = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Content>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>>(({ className, ...props }, ref) => <TabsPrimitive.Content ref={ref} className={cn("mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", className)} {...props} />);
19
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
20
+
21
+ export { Tabs, TabsContent, TabsList, TabsTrigger };
22
+
frontend/src/components/ui/textarea.tsx ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { useImperativeHandle } from "react";
5
+
6
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
7
+
8
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
9
+ return <textarea className={cn("flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className)} ref={ref} {...props} />;
10
+ });
11
+ Textarea.displayName = "Textarea";
12
+
13
+ export { Textarea };
14
+
15
+ interface UseAutosizeTextAreaProps {
16
+ textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
17
+ minHeight?: number;
18
+ maxHeight?: number;
19
+ triggerAutoSize: string;
20
+ }
21
+
22
+ export const useAutosizeTextArea = ({ textAreaRef, triggerAutoSize, maxHeight = Number.MAX_SAFE_INTEGER, minHeight = 0 }: UseAutosizeTextAreaProps) => {
23
+ const [init, setInit] = React.useState(true);
24
+ React.useEffect(() => {
25
+ // We need to reset the height momentarily to get the correct scrollHeight for the textarea
26
+ const offsetBorder = 0;
27
+ const textAreaElement = textAreaRef.current;
28
+ if (textAreaElement) {
29
+ if (init) {
30
+ textAreaElement.style.minHeight = `${minHeight + offsetBorder}px`;
31
+ if (maxHeight > minHeight) {
32
+ textAreaElement.style.maxHeight = `${maxHeight}px`;
33
+ }
34
+ setInit(false);
35
+ }
36
+ textAreaElement.style.height = `${minHeight + offsetBorder}px`;
37
+ const scrollHeight = textAreaElement.scrollHeight;
38
+ // We then set the height directly, outside of the render loop
39
+ // Trying to set this with state or a ref will product an incorrect value.
40
+ if (scrollHeight > maxHeight) {
41
+ textAreaElement.style.height = `${maxHeight}px`;
42
+ } else {
43
+ textAreaElement.style.height = `${scrollHeight + offsetBorder}px`;
44
+ }
45
+ }
46
+ }, [textAreaRef.current, triggerAutoSize]);
47
+ };
48
+
49
+ export type AutosizeTextAreaRef = {
50
+ textArea: HTMLTextAreaElement;
51
+ maxHeight: number;
52
+ minHeight: number;
53
+ };
54
+
55
+ type AutosizeTextAreaProps = {
56
+ maxHeight?: number;
57
+ minHeight?: number;
58
+ } & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
59
+
60
+ export const AutosizeTextarea = React.forwardRef<AutosizeTextAreaRef, AutosizeTextAreaProps>(({ maxHeight = Number.MAX_SAFE_INTEGER, minHeight = 52, className, onChange, value, ...props }: AutosizeTextAreaProps, ref: React.Ref<AutosizeTextAreaRef>) => {
61
+ const textAreaRef = React.useRef<HTMLTextAreaElement | null>(null);
62
+ const [triggerAutoSize, setTriggerAutoSize] = React.useState("");
63
+
64
+ useAutosizeTextArea({
65
+ textAreaRef,
66
+ triggerAutoSize: triggerAutoSize,
67
+ maxHeight,
68
+ minHeight,
69
+ });
70
+
71
+ useImperativeHandle(ref, () => ({
72
+ textArea: textAreaRef.current as HTMLTextAreaElement,
73
+ focus: () => textAreaRef?.current?.focus(),
74
+ maxHeight,
75
+ minHeight,
76
+ }));
77
+
78
+ React.useEffect(() => {
79
+ setTriggerAutoSize(value as string);
80
+ }, [props?.defaultValue, value]);
81
+
82
+ return (
83
+ <textarea
84
+ {...props}
85
+ value={value}
86
+ ref={textAreaRef}
87
+ className={cn("flex w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", className)}
88
+ onChange={(e) => {
89
+ setTriggerAutoSize(e.target.value);
90
+ onChange?.(e);
91
+ }}
92
+ />
93
+ );
94
+ });
95
+ AutosizeTextarea.displayName = "AutosizeTextarea";
frontend/src/components/ui/tooltip.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Portal>
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ </TooltipPrimitive.Portal>
29
+ ))
30
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
31
+
32
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
frontend/src/lib/socket.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { io, Socket } from "socket.io-client";
2
+
3
+ let socket: Socket | null = null;
4
+
5
+ export const initializeSocket = (url: string = process.env.NEXT_PUBLIC_KNET_BACKEND!) => {
6
+ url = process.env.NEXT_PUBLIC_KNET_BACKEND || "http://127.0.0.1:5000";
7
+ if (!socket) {
8
+ socket = io(url, {
9
+ transports: ["websocket"],
10
+ reconnection: true,
11
+ reconnectionAttempts: 5,
12
+ reconnectionDelay: 1000,
13
+ });
14
+ }
15
+ return socket;
16
+ };
17
+
18
+ export const getSocket = () => {
19
+ if (!socket) {
20
+ throw new Error("Socket not initialized. Call initializeSocket first.");
21
+ }
22
+ return socket;
23
+ };
24
+
25
+ export const disconnectSocket = () => {
26
+ if (socket) {
27
+ socket.disconnect();
28
+ socket = null;
29
+ }
30
+ };
frontend/src/lib/types.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Message {
2
+ id: string;
3
+ content: string;
4
+ role: "user" | "assistant" | "system";
5
+ timestamp: Date;
6
+ }
7
+
8
+ export interface ChatState {
9
+ messages: Message[];
10
+ isLoading: boolean;
11
+ error: string | null;
12
+ }
13
+
14
+ export interface ResearchOptions {
15
+ depth: "basic" | "intermediate" | "deep";
16
+ sources: boolean;
17
+ citations: boolean;
18
+ }
19
+
20
+ export interface ChatData {
21
+ conversations: Conversation[];
22
+ currentConversationId: string | null;
23
+ }
24
+
25
+ export interface Conversation {
26
+ id: string;
27
+ title: string;
28
+ lastUpdated: string;
29
+ messages: Message[];
30
+ active: boolean;
31
+ }
32
+
33
+ export interface StatusUpdate {
34
+ message: string;
35
+ progress: number;
36
+ }
37
+
38
+ export interface ResearchNode {
39
+ query: string;
40
+ children?: ResearchNode[];
41
+ }
42
+
43
+ export interface ResearchMetadata {
44
+ total_queries: number;
45
+ total_sources: number;
46
+ max_depth_reached: number;
47
+ total_tokens: number;
48
+ }
49
+
50
+ export interface ResearchResults {
51
+ topic: string;
52
+ timestamp: string;
53
+ content: string;
54
+ media?: {
55
+ images?: string[];
56
+ videos?: string[];
57
+ };
58
+ research_tree: ResearchNode;
59
+ metadata: ResearchMetadata;
60
+ }
frontend/src/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
frontend/tailwind.config.ts CHANGED
@@ -1,19 +1,63 @@
1
  import type { Config } from "tailwindcss";
2
 
3
  const config: Config = {
4
- content: [
 
5
  "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6
  "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7
  "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8
  ],
9
  theme: {
10
- extend: {
11
- colors: {
12
- background: "var(--background)",
13
- foreground: "var(--foreground)",
14
- },
15
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  },
17
- plugins: [],
18
  };
19
  export default config;
 
1
  import type { Config } from "tailwindcss";
2
 
3
  const config: Config = {
4
+ darkMode: ["class"],
5
+ content: [
6
  "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7
  "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8
  "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9
  ],
10
  theme: {
11
+ extend: {
12
+ colors: {
13
+ background: 'hsl(var(--background))',
14
+ foreground: 'hsl(var(--foreground))',
15
+ card: {
16
+ DEFAULT: 'hsl(var(--card))',
17
+ foreground: 'hsl(var(--card-foreground))'
18
+ },
19
+ popover: {
20
+ DEFAULT: 'hsl(var(--popover))',
21
+ foreground: 'hsl(var(--popover-foreground))'
22
+ },
23
+ primary: {
24
+ DEFAULT: 'hsl(var(--primary))',
25
+ foreground: 'hsl(var(--primary-foreground))'
26
+ },
27
+ secondary: {
28
+ DEFAULT: 'hsl(var(--secondary))',
29
+ foreground: 'hsl(var(--secondary-foreground))'
30
+ },
31
+ muted: {
32
+ DEFAULT: 'hsl(var(--muted))',
33
+ foreground: 'hsl(var(--muted-foreground))'
34
+ },
35
+ accent: {
36
+ DEFAULT: 'hsl(var(--accent))',
37
+ foreground: 'hsl(var(--accent-foreground))'
38
+ },
39
+ destructive: {
40
+ DEFAULT: 'hsl(var(--destructive))',
41
+ foreground: 'hsl(var(--destructive-foreground))'
42
+ },
43
+ border: 'hsl(var(--border))',
44
+ input: 'hsl(var(--input))',
45
+ ring: 'hsl(var(--ring))',
46
+ chart: {
47
+ '1': 'hsl(var(--chart-1))',
48
+ '2': 'hsl(var(--chart-2))',
49
+ '3': 'hsl(var(--chart-3))',
50
+ '4': 'hsl(var(--chart-4))',
51
+ '5': 'hsl(var(--chart-5))'
52
+ }
53
+ },
54
+ borderRadius: {
55
+ lg: 'var(--radius)',
56
+ md: 'calc(var(--radius) - 2px)',
57
+ sm: 'calc(var(--radius) - 4px)'
58
+ }
59
+ }
60
  },
61
+ plugins: [require("tailwindcss-animate")],
62
  };
63
  export default config;