Soham Waghmare commited on
Commit
b636e8f
·
1 Parent(s): 514da67

feat: emit statespace graph for explainability

Browse files
backend/knet.py CHANGED
@@ -149,17 +149,18 @@ class Schema:
149
 
150
 
151
  class ResearchProgress:
152
- def __init__(self, callback):
153
  self.progress = 0
154
  self.callback = callback
 
155
 
156
  async def update(self, progress: int, message: str):
157
  self.progress = int(min(100, self.progress + progress)) # max 100
158
- await self.callback({"progress": self.progress, "message": message})
159
 
160
  async def setter(self, progress: int, message: str):
161
  self.progress = int(min(100, progress)) # max 100
162
- await self.callback({"progress": self.progress, "message": message})
163
 
164
 
165
  class KNet:
@@ -180,6 +181,7 @@ class KNet:
180
  self.num_sites_per_query = num_sites_per_query
181
 
182
  # Global State
 
183
  self.research_plan: list[str] = []
184
  self.idx_research_plan: int = 0
185
  self.ctx_researcher: list[str] = []
@@ -188,7 +190,7 @@ class KNet:
188
 
189
  async def conduct_research(self, topic: str, progress_callback, max_depth: int, num_sites_per_query: int) -> dict | bool:
190
  # Local Runtime State
191
- self.progress = ResearchProgress(progress_callback)
192
  self.max_depth = max_depth
193
  self.num_sites_per_query = num_sites_per_query
194
 
@@ -209,8 +211,6 @@ class KNet:
209
  ]
210
  self.logger.info(f"Research plan:\n{json.dumps(self.research_plan, indent=2)}")
211
 
212
- master_node = ResearchNode()
213
-
214
  await self.progress.update(0, "Starting research...")
215
 
216
  # Iterate on research plan
@@ -227,7 +227,7 @@ class KNet:
227
  )["branches"][0]
228
 
229
  root_node = ResearchNode(query)
230
- master_node.add_child(root_node.query, node=root_node)
231
  to_explore = deque([(root_node, 1)]) # (node, depth) pairs
232
  explored_queries = set() # {string, string, ...}
233
 
@@ -261,9 +261,9 @@ class KNet:
261
 
262
  # Generate final report
263
  await self.progress.update(100 / (len(self.research_plan) + 1), "Generating final report...")
264
- final_report = await self._generate_final_report(master_node, topic)
265
 
266
- self.logger.info(f"Research completed. Explored {len(explored_queries)} queries across {master_node.max_depth()} levels")
267
  await self.progress.update(100, "Research complete!")
268
 
269
  with open("output.log.json", "w", encoding="utf-8") as f:
@@ -282,7 +282,7 @@ class KNet:
282
  if asyncio.current_task() and asyncio.current_task().cancelled():
283
  raise asyncio.CancelledError("Research task was cancelled")
284
 
285
- async def _generate_final_report(self, root_node: ResearchNode, topic: str, retry_count: int = 1) -> Dict[str, Any]:
286
  try:
287
  self._check_cancelled()
288
 
@@ -322,7 +322,7 @@ class KNet:
322
 
323
  # Collate multimedia content
324
  media_content = {"images": [], "videos": [], "links": []}
325
- all_sources_data = root_node.get_all_data()
326
  for data in all_sources_data:
327
  if data.get("images"):
328
  media_content["images"].extend(data["images"])
@@ -336,28 +336,16 @@ class KNet:
336
  media_content["links"] = list({json.dumps(d, sort_keys=True) for d in media_content["links"]})
337
  media_content["links"] = [json.loads(d) for d in media_content["links"]]
338
 
339
- # Build research tree structure
340
- def build_tree_structure(node: ResearchNode) -> Dict:
341
- if not node:
342
- return {}
343
- sources = [d["url"] for d in node.data if d.get("url")]
344
- return {
345
- "query": node.query,
346
- "depth": node.depth,
347
- "sources": sources,
348
- "children": [build_tree_structure(child) for child in node.children],
349
- }
350
-
351
  return {
352
  "topic": topic,
353
  "timestamp": datetime.now().isoformat(),
354
  "content": raster_report,
355
  "media": media_content,
356
- "research_tree": build_tree_structure(root_node),
357
  "metadata": {
358
- "total_queries": root_node.total_children(),
359
  "total_sources": len(all_sources_data),
360
- "max_depth_reached": root_node.max_depth(),
361
  "total_tokens": self.token_count,
362
  },
363
  }
@@ -369,7 +357,7 @@ class KNet:
369
  self.logger.error("GEMINI_RECITATION or NO_RESPONSE")
370
  if retry_count < 3:
371
  self.logger.error(f"Retrying final report:C:{retry_count} / 3", exc_info=True)
372
- return await self._generate_final_report(root_node, topic, retry_count + 1)
373
  self.logger.error("Error generating final report", exc_info=True)
374
  raise
375
 
@@ -470,7 +458,7 @@ class KNet:
470
  raise
471
 
472
  async def test(self, topic: str, progress_callback):
473
- self.progress = ResearchProgress(progress_callback)
474
  try:
475
  for i in range(5):
476
  self._check_cancelled()
 
149
 
150
 
151
  class ResearchProgress:
152
+ def __init__(self, callback, master_node: ResearchNode):
153
  self.progress = 0
154
  self.callback = callback
155
+ self.master_node = master_node
156
 
157
  async def update(self, progress: int, message: str):
158
  self.progress = int(min(100, self.progress + progress)) # max 100
159
+ await self.callback({"progress": self.progress, "message": message, "research_tree": self.master_node.build_tree_structure()})
160
 
161
  async def setter(self, progress: int, message: str):
162
  self.progress = int(min(100, progress)) # max 100
163
+ await self.callback({"progress": self.progress, "message": message, "research_tree": self.master_node.build_tree_structure()})
164
 
165
 
166
  class KNet:
 
181
  self.num_sites_per_query = num_sites_per_query
182
 
183
  # Global State
184
+ self.master_node = ResearchNode()
185
  self.research_plan: list[str] = []
186
  self.idx_research_plan: int = 0
187
  self.ctx_researcher: list[str] = []
 
190
 
191
  async def conduct_research(self, topic: str, progress_callback, max_depth: int, num_sites_per_query: int) -> dict | bool:
192
  # Local Runtime State
193
+ self.progress = ResearchProgress(progress_callback, self.master_node)
194
  self.max_depth = max_depth
195
  self.num_sites_per_query = num_sites_per_query
196
 
 
211
  ]
212
  self.logger.info(f"Research plan:\n{json.dumps(self.research_plan, indent=2)}")
213
 
 
 
214
  await self.progress.update(0, "Starting research...")
215
 
216
  # Iterate on research plan
 
227
  )["branches"][0]
228
 
229
  root_node = ResearchNode(query)
230
+ self.master_node.add_child(root_node.query, node=root_node)
231
  to_explore = deque([(root_node, 1)]) # (node, depth) pairs
232
  explored_queries = set() # {string, string, ...}
233
 
 
261
 
262
  # Generate final report
263
  await self.progress.update(100 / (len(self.research_plan) + 1), "Generating final report...")
264
+ final_report = await self._generate_final_report(topic)
265
 
266
+ self.logger.info(f"Research completed. Explored {len(explored_queries)} queries across {self.master_node.max_depth()} levels")
267
  await self.progress.update(100, "Research complete!")
268
 
269
  with open("output.log.json", "w", encoding="utf-8") as f:
 
282
  if asyncio.current_task() and asyncio.current_task().cancelled():
283
  raise asyncio.CancelledError("Research task was cancelled")
284
 
285
+ async def _generate_final_report(self, topic: str, retry_count: int = 1) -> Dict[str, Any]:
286
  try:
287
  self._check_cancelled()
288
 
 
322
 
323
  # Collate multimedia content
324
  media_content = {"images": [], "videos": [], "links": []}
325
+ all_sources_data = self.master_node.get_all_data()
326
  for data in all_sources_data:
327
  if data.get("images"):
328
  media_content["images"].extend(data["images"])
 
336
  media_content["links"] = list({json.dumps(d, sort_keys=True) for d in media_content["links"]})
337
  media_content["links"] = [json.loads(d) for d in media_content["links"]]
338
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  return {
340
  "topic": topic,
341
  "timestamp": datetime.now().isoformat(),
342
  "content": raster_report,
343
  "media": media_content,
344
+ "research_tree": self.master_node.build_tree_structure(),
345
  "metadata": {
346
+ "total_queries": self.master_node.total_children(),
347
  "total_sources": len(all_sources_data),
348
+ "max_depth_reached": self.master_node.max_depth(),
349
  "total_tokens": self.token_count,
350
  },
351
  }
 
357
  self.logger.error("GEMINI_RECITATION or NO_RESPONSE")
358
  if retry_count < 3:
359
  self.logger.error(f"Retrying final report:C:{retry_count} / 3", exc_info=True)
360
+ return await self._generate_final_report(topic, retry_count + 1)
361
  self.logger.error("Error generating final report", exc_info=True)
362
  raise
363
 
 
458
  raise
459
 
460
  async def test(self, topic: str, progress_callback):
461
+ self.progress = ResearchProgress(progress_callback, self.master_node)
462
  try:
463
  for i in range(5):
464
  self._check_cancelled()
backend/research_node.py CHANGED
@@ -1,16 +1,16 @@
1
  import copy
2
- from typing import Any, Dict, List, Optional
3
 
4
 
5
  class ResearchNode:
6
- def __init__(self, query: str = "_", parent: Optional["ResearchNode"] = None, depth: int = 0):
7
  self.query = query
8
  self.parent = parent
9
  self.depth = depth
10
  self.children: List[ResearchNode] = []
11
  self.data: List[Dict[str, Any]] = []
12
 
13
- def add_child(self, query: str, node: Optional["ResearchNode"] = None) -> "ResearchNode":
14
  if node:
15
  child = node
16
  child.parent = self
@@ -48,10 +48,14 @@ class ResearchNode:
48
  data.extend(child.get_all_data())
49
  return data
50
 
51
- def __repr__(self) -> dict:
 
 
 
 
52
  return {
53
  "query": self.query,
54
  "depth": self.depth,
55
- "children": [child.__repr__() for child in self.children],
56
- "data": self.data,
57
  }
 
1
  import copy
2
+ from typing import Any, Dict, List, Optional, Self
3
 
4
 
5
  class ResearchNode:
6
+ def __init__(self, query: str = "_", parent: Optional[Self] = None, depth: int = 0):
7
  self.query = query
8
  self.parent = parent
9
  self.depth = depth
10
  self.children: List[ResearchNode] = []
11
  self.data: List[Dict[str, Any]] = []
12
 
13
+ def add_child(self, query: str, node: Optional[Self] = None) -> Self:
14
  if node:
15
  child = node
16
  child.parent = self
 
48
  data.extend(child.get_all_data())
49
  return data
50
 
51
+ # Build research tree structure
52
+ def build_tree_structure(self) -> Dict:
53
+ if not self:
54
+ return {}
55
+ sources = {d["url"]: d["text"] for d in self.data if d.get("url") and d.get("text")}
56
  return {
57
  "query": self.query,
58
  "depth": self.depth,
59
+ "sources": sources,
60
+ "children": [child.build_tree_structure() for child in self.children],
61
  }
frontend/bun.lock CHANGED
@@ -18,6 +18,7 @@
18
  "@types/react-syntax-highlighter": "^15.5.13",
19
  "class-variance-authority": "^0.7.1",
20
  "clsx": "^2.1.1",
 
21
  "eslint-config-next": "^15.2.4",
22
  "lucide-react": "^0.479.0",
23
  "next": "^15.2.4",
@@ -32,9 +33,11 @@
32
  "socket.io-client": "^4.8.1",
33
  "tailwind-merge": "^3.0.2",
34
  "tailwindcss-animate": "^1.0.7",
 
35
  "uuid": "^11.1.0",
36
  },
37
  "devDependencies": {
 
38
  "@types/node": "^20.17.28",
39
  "@types/react": "^18.3.20",
40
  "@types/react-dom": "^18.3.5",
@@ -336,12 +339,76 @@
336
 
337
  "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
340
 
341
  "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
342
 
343
  "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
344
 
 
 
345
  "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
346
 
347
  "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -562,6 +629,68 @@
562
 
563
  "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
564
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
566
 
567
  "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
@@ -586,6 +715,8 @@
586
 
587
  "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
588
 
 
 
589
  "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
590
 
591
  "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
@@ -788,6 +919,8 @@
788
 
789
  "human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="],
790
 
 
 
791
  "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
792
 
793
  "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
@@ -802,6 +935,8 @@
802
 
803
  "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
804
 
 
 
805
  "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
806
 
807
  "is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
@@ -1224,8 +1359,12 @@
1224
 
1225
  "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
1226
 
 
 
1227
  "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
1228
 
 
 
1229
  "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
1230
 
1231
  "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -1234,6 +1373,8 @@
1234
 
1235
  "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
1236
 
 
 
1237
  "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
1238
 
1239
  "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -1340,6 +1481,8 @@
1340
 
1341
  "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
1342
 
 
 
1343
  "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
1344
 
1345
  "tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
@@ -1510,6 +1653,8 @@
1510
 
1511
  "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1512
 
 
 
1513
  "decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
1514
 
1515
  "eslint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
 
18
  "@types/react-syntax-highlighter": "^15.5.13",
19
  "class-variance-authority": "^0.7.1",
20
  "clsx": "^2.1.1",
21
+ "d3": "^7.9.0",
22
  "eslint-config-next": "^15.2.4",
23
  "lucide-react": "^0.479.0",
24
  "next": "^15.2.4",
 
33
  "socket.io-client": "^4.8.1",
34
  "tailwind-merge": "^3.0.2",
35
  "tailwindcss-animate": "^1.0.7",
36
+ "three": "^0.175.0",
37
  "uuid": "^11.1.0",
38
  },
39
  "devDependencies": {
40
+ "@types/d3": "^7.4.3",
41
  "@types/node": "^20.17.28",
42
  "@types/react": "^18.3.20",
43
  "@types/react-dom": "^18.3.5",
 
339
 
340
  "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
341
 
342
+ "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
343
+
344
+ "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
345
+
346
+ "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
347
+
348
+ "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
349
+
350
+ "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
351
+
352
+ "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
353
+
354
+ "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
355
+
356
+ "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
357
+
358
+ "@types/d3-dispatch": ["@types/d3-dispatch@3.0.6", "", {}, "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ=="],
359
+
360
+ "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
361
+
362
+ "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
363
+
364
+ "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
365
+
366
+ "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
367
+
368
+ "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
369
+
370
+ "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
371
+
372
+ "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
373
+
374
+ "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
375
+
376
+ "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
377
+
378
+ "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
379
+
380
+ "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
381
+
382
+ "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
383
+
384
+ "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
385
+
386
+ "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
387
+
388
+ "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
389
+
390
+ "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
391
+
392
+ "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
393
+
394
+ "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
395
+
396
+ "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
397
+
398
+ "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
399
+
400
+ "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
401
+
402
+ "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
403
+
404
  "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
405
 
406
  "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
407
 
408
  "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
409
 
410
+ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
411
+
412
  "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
413
 
414
  "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 
629
 
630
  "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
631
 
632
+ "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
633
+
634
+ "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
635
+
636
+ "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
637
+
638
+ "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
639
+
640
+ "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
641
+
642
+ "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
643
+
644
+ "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
645
+
646
+ "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
647
+
648
+ "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
649
+
650
+ "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
651
+
652
+ "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
653
+
654
+ "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
655
+
656
+ "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
657
+
658
+ "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
659
+
660
+ "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
661
+
662
+ "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
663
+
664
+ "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
665
+
666
+ "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
667
+
668
+ "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
669
+
670
+ "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
671
+
672
+ "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
673
+
674
+ "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
675
+
676
+ "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
677
+
678
+ "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
679
+
680
+ "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
681
+
682
+ "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
683
+
684
+ "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
685
+
686
+ "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
687
+
688
+ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
689
+
690
+ "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
691
+
692
+ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
693
+
694
  "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
695
 
696
  "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
 
715
 
716
  "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
717
 
718
+ "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
719
+
720
  "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
721
 
722
  "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
 
919
 
920
  "human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="],
921
 
922
+ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
923
+
924
  "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
925
 
926
  "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 
935
 
936
  "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
937
 
938
+ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
939
+
940
  "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
941
 
942
  "is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
 
1359
 
1360
  "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
1361
 
1362
+ "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
1363
+
1364
  "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
1365
 
1366
+ "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
1367
+
1368
  "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
1369
 
1370
  "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
 
1373
 
1374
  "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
1375
 
1376
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
1377
+
1378
  "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
1379
 
1380
  "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
1481
 
1482
  "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
1483
 
1484
+ "three": ["three@0.175.0", "", {}, "sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg=="],
1485
+
1486
  "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
1487
 
1488
  "tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
 
1653
 
1654
  "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1655
 
1656
+ "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
1657
+
1658
  "decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
1659
 
1660
  "eslint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
frontend/package.json CHANGED
@@ -23,6 +23,7 @@
23
  "@types/react-syntax-highlighter": "^15.5.13",
24
  "class-variance-authority": "^0.7.1",
25
  "clsx": "^2.1.1",
 
26
  "eslint-config-next": "^15.2.4",
27
  "lucide-react": "^0.479.0",
28
  "next": "^15.2.4",
@@ -37,9 +38,11 @@
37
  "socket.io-client": "^4.8.1",
38
  "tailwind-merge": "^3.0.2",
39
  "tailwindcss-animate": "^1.0.7",
 
40
  "uuid": "^11.1.0"
41
  },
42
  "devDependencies": {
 
43
  "@types/node": "^20.17.28",
44
  "@types/react": "^18.3.20",
45
  "@types/react-dom": "^18.3.5",
 
23
  "@types/react-syntax-highlighter": "^15.5.13",
24
  "class-variance-authority": "^0.7.1",
25
  "clsx": "^2.1.1",
26
+ "d3": "^7.9.0",
27
  "eslint-config-next": "^15.2.4",
28
  "lucide-react": "^0.479.0",
29
  "next": "^15.2.4",
 
38
  "socket.io-client": "^4.8.1",
39
  "tailwind-merge": "^3.0.2",
40
  "tailwindcss-animate": "^1.0.7",
41
+ "three": "^0.175.0",
42
  "uuid": "^11.1.0"
43
  },
44
  "devDependencies": {
45
+ "@types/d3": "^7.4.3",
46
  "@types/node": "^20.17.28",
47
  "@types/react": "^18.3.20",
48
  "@types/react-dom": "^18.3.5",
frontend/src/components/ui/ChatLayout.tsx CHANGED
@@ -1,14 +1,17 @@
 
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6
- import { LayoutGrid, Menu, MessageCircle, Settings } from "lucide-react";
7
- import React from "react";
8
  import { ThemeToggle } from "./ThemeToggle";
9
  import Link from "next/link";
10
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
11
  import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription } from "@/components/ui/sheet";
 
 
12
 
13
  interface ChatLayoutProps {
14
  sidebar: React.ReactNode;
@@ -17,6 +20,25 @@ interface ChatLayoutProps {
17
  }
18
 
19
  const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsPanel }) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  return (
21
  <div className="h-screen flex flex-col">
22
  <header className="border-b-2 h-14 flex items-center px-6">
@@ -70,7 +92,7 @@ const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsP
70
  <ResizableHandle withHandle className="hidden md:flex" />
71
 
72
  <ResizablePanel defaultSize={85} className="w-full md:w-auto">
73
- <Tabs defaultValue="chat" className="h-full flex flex-col">
74
  <div className="p-4">
75
  <TabsList className="">
76
  <TabsTrigger value="chat" className="flex items-center gap-2">
@@ -84,18 +106,12 @@ const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsP
84
  </TabsList>
85
  </div>
86
 
87
- <TabsContent value="chat" className="overflow-auto flex flex-1 !mt-0" tabIndex={-1}>
88
  {mainContent}
89
  </TabsContent>
90
 
91
- <TabsContent value="visualizations" className="p-4 flex-1">
92
- <div className="h-full flex items-center justify-center text-muted-foreground">
93
- <div className="text-center">
94
- <LayoutGrid className="mx-auto h-12 w-12 mb-4 opacity-30" />
95
- <h3 className="text-lg font-medium mb-2">Visualizations Coming Soon</h3>
96
- <p>View graphs, charts, and other visual representations of your research data.</p>
97
- </div>
98
- </div>
99
  </TabsContent>
100
  </Tabs>
101
  </ResizablePanel>
 
1
+ "use client";
2
  import { Button } from "@/components/ui/button";
3
  import { Card } from "@/components/ui/card";
4
  import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
5
  import { ScrollArea } from "@/components/ui/scroll-area";
6
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
7
+ import { LayoutGrid, Menu, MessageCircle, Network, Settings } from "lucide-react";
8
+ import React, { useState } from "react";
9
  import { ThemeToggle } from "./ThemeToggle";
10
  import Link from "next/link";
11
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
12
  import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription } from "@/components/ui/sheet";
13
+ import ResearchGraph from "@/components/visualizations/ResearchGraph";
14
+ import { useChatContext } from "@/lib/store/ChatContext";
15
 
16
  interface ChatLayoutProps {
17
  sidebar: React.ReactNode;
 
20
  }
21
 
22
  const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsPanel }) => {
23
+ const { chatState } = useChatContext();
24
+ const [activeTab, setActiveTab] = useState<string>("chat");
25
+ const [visualizationType, setVisualizationType] = useState<"d3" | "reactflow">("d3");
26
+
27
+ // Get the latest research tree from messages
28
+ const latestResearchTree = React.useMemo(() => {
29
+ // First look for the most recent completed research message
30
+ const completedResearch = [...chatState.messages].reverse().find((msg) => msg.role === "assistant" && !msg.isProgress && msg.research_tree);
31
+
32
+ if (completedResearch?.research_tree) {
33
+ return completedResearch.research_tree;
34
+ }
35
+
36
+ // If no completed research, look for progress messages
37
+ const progressMessage = [...chatState.messages].reverse().find((msg) => msg.role === "assistant" && msg.isProgress && msg.research_tree);
38
+
39
+ return progressMessage?.research_tree;
40
+ }, [chatState.messages]);
41
+
42
  return (
43
  <div className="h-screen flex flex-col">
44
  <header className="border-b-2 h-14 flex items-center px-6">
 
92
  <ResizableHandle withHandle className="hidden md:flex" />
93
 
94
  <ResizablePanel defaultSize={85} className="w-full md:w-auto">
95
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
96
  <div className="p-4">
97
  <TabsList className="">
98
  <TabsTrigger value="chat" className="flex items-center gap-2">
 
106
  </TabsList>
107
  </div>
108
 
109
+ <TabsContent value="chat" className="overflow-auto flex-1 !mt-0" tabIndex={-1}>
110
  {mainContent}
111
  </TabsContent>
112
 
113
+ <TabsContent value="visualizations" className="p-4 pt-0 overflow-auto flex-1 !mt-0" tabIndex={-1}>
114
+ <ResearchGraph researchTree={latestResearchTree} />
 
 
 
 
 
 
115
  </TabsContent>
116
  </Tabs>
117
  </ResizablePanel>
frontend/src/components/visualizations/ResearchGraph.tsx ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { Card, CardContent, CardTitle } from "@/components/ui/card";
4
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
5
+ import { ResearchTree } from "@/lib/types";
6
+ import * as d3 from "d3";
7
+ import React, { useEffect, useRef, useState } from "react";
8
+
9
+ interface GraphNode extends d3.SimulationNodeDatum {
10
+ id: string;
11
+ name: string;
12
+ size: number;
13
+ depth: number;
14
+ url?: string;
15
+ width?: number;
16
+ height?: number;
17
+ sources?: Record<string, string>;
18
+ }
19
+
20
+ interface GraphLink extends d3.SimulationLinkDatum<GraphNode> {
21
+ source: string | GraphNode;
22
+ target: string | GraphNode;
23
+ }
24
+
25
+ interface ResearchGraphProps {
26
+ researchTree: ResearchTree | undefined;
27
+ }
28
+
29
+ const ResearchGraph: React.FC<ResearchGraphProps> = ({ researchTree }) => {
30
+ const selectedNodeRef = useRef<GraphNode | null>(null);
31
+ const [dialogOpen, setDialogOpen] = useState(false);
32
+ const [selectedNodeContent, setSelectedNodeContent] = useState<{ title: string; sources: Record<string, string> }>({
33
+ title: "",
34
+ sources: {},
35
+ });
36
+ const [nodePositions, setNodePositions] = useState<Record<string, { x: number; y: number }>>({});
37
+ const [nodesState, setNodesState] = useState<GraphNode[]>([]); // Store nodes for both D3 and rendering
38
+ const containerRef = useRef<HTMLDivElement>(null);
39
+ const [dimensions, setDimensions] = useState({ width: 1200, height: 800 });
40
+
41
+ // Resize observer to update graph size
42
+ useEffect(() => {
43
+ if (!containerRef.current) return;
44
+ const handleResize = () => {
45
+ const rect = containerRef.current?.getBoundingClientRect();
46
+ if (rect) {
47
+ setDimensions({ width: rect.width, height: rect.height });
48
+ }
49
+ };
50
+ handleResize();
51
+ window.addEventListener("resize", handleResize);
52
+ return () => window.removeEventListener("resize", handleResize);
53
+ }, []);
54
+
55
+ useEffect(() => {
56
+ if (!researchTree) return;
57
+
58
+ // Prepare data
59
+ const nodes: GraphNode[] = [];
60
+ const links: GraphLink[] = [];
61
+
62
+ const processTree = (tree: ResearchTree, parentId?: string) => {
63
+ const nodeId = `${tree.query}_${tree.depth}`;
64
+ const textLength = tree.query.length;
65
+ const nodeWidth = Math.max(120, Math.min(300, textLength * 6));
66
+ const nodeHeight = 60;
67
+
68
+ nodes.push({
69
+ id: nodeId,
70
+ name: tree.query,
71
+ size: Object.keys(tree.sources).length + 10,
72
+ depth: tree.depth,
73
+ width: nodeWidth,
74
+ height: nodeHeight,
75
+ sources: tree.sources,
76
+ });
77
+
78
+ if (parentId) {
79
+ links.push({
80
+ source: parentId,
81
+ target: nodeId,
82
+ });
83
+ }
84
+
85
+ tree.children.forEach((child) => {
86
+ processTree(child, nodeId);
87
+ });
88
+ };
89
+
90
+ processTree(researchTree);
91
+ setNodesState([...nodes]); // Save nodes for rendering
92
+
93
+ const { width, height } = dimensions;
94
+ const simulation = d3
95
+ .forceSimulation<GraphNode>(nodes)
96
+ .force(
97
+ "link",
98
+ d3
99
+ .forceLink<GraphNode, GraphLink>()
100
+ .id((d: GraphNode) => d.id)
101
+ .links(links)
102
+ .distance(80)
103
+ )
104
+ .force("charge", d3.forceManyBody().strength(-800))
105
+ .force("center", d3.forceCenter(width / 2, height / 2))
106
+ .force(
107
+ "collision",
108
+ d3.forceCollide().radius((d: d3.SimulationNodeDatum) => {
109
+ const node = d as GraphNode;
110
+ return Math.max(node.width || 0, node.height || 0) / 2 + 40;
111
+ })
112
+ )
113
+ .on("tick", () => {
114
+ setNodePositions((prev) => {
115
+ const updated: Record<string, { x: number; y: number }> = {};
116
+ nodes.forEach((n) => {
117
+ // Clamp positions to container
118
+ updated[n.id] = {
119
+ x: Math.max((n.width || 120) / 2, Math.min(n.x || 0, width - (n.width || 120) / 2)),
120
+ y: Math.max((n.height || 60) / 2, Math.min(n.y || 0, height - (n.height || 60) / 2)),
121
+ };
122
+ });
123
+ return updated;
124
+ });
125
+ });
126
+
127
+ return () => {
128
+ simulation.stop();
129
+ };
130
+ }, [researchTree, dimensions]);
131
+
132
+ const formatSourceContent = () => {
133
+ return Object.entries(selectedNodeContent.sources).map(([url, content]) => (
134
+ <div key={url} className="mb-6 border-b pb-4">
135
+ <h3 className="text-base font-semibold mb-2">
136
+ <a href={url} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline break-words">
137
+ {url}
138
+ </a>
139
+ </h3>
140
+ <div className="prose prose-sm dark:prose-invert max-w-none break-words">{content}</div>
141
+ </div>
142
+ ));
143
+ };
144
+
145
+ const renderNodeCard = (node: GraphNode) => {
146
+ const pos = nodePositions[node.id] || { x: 0, y: 0 };
147
+ return (
148
+ <div
149
+ key={node.id}
150
+ style={{
151
+ position: "absolute",
152
+ left: pos.x - (node.width || 120) / 2,
153
+ top: pos.y - (node.height || 60) / 2,
154
+ width: node.width || 120,
155
+ zIndex: 2,
156
+ cursor: node.url ? "pointer" : "default",
157
+ userSelect: "none",
158
+ pointerEvents: "auto",
159
+ }}
160
+ tabIndex={0}
161
+ className="select-none focus:outline-none"
162
+ onClick={(e) => {
163
+ e.stopPropagation();
164
+ selectedNodeRef.current = node;
165
+ if (node.url) {
166
+ window.open(node.url, "_blank");
167
+ } else if (node.sources && Object.keys(node.sources).length > 0) {
168
+ setSelectedNodeContent({ title: node.name, sources: node.sources });
169
+ setDialogOpen(true);
170
+ }
171
+ }}
172
+ onMouseDown={(e) => e.stopPropagation()}>
173
+ <Card className={"bg-muted text-white cursor-pointer" + (node.name === "_" ? " w-32 h-[5rem]" : "")}>
174
+ <CardTitle className={`p-2 pb-0 text-sm ${node.name === "_" ? "text-lg grid place-items-center h-4/5 w-full" : ""}`} title={node.name}>
175
+ {node.name === "_" ? "Master Node" : node.name}
176
+ </CardTitle>
177
+ {node.name !== "_" && (
178
+ <CardContent className="p-2 flex flex-wrap gap-1 w-full">
179
+ <Badge variant="secondary" className="w-full bg-white/20 text-white border-white/20 grid place-items-center" title={node.name}>
180
+ Source
181
+ </Badge>
182
+ </CardContent>
183
+ )}
184
+ </Card>
185
+ </div>
186
+ );
187
+ };
188
+
189
+ return (
190
+ <div ref={containerRef} className="flex flex-col h-full flex-1 relative" style={{ minHeight: 600 }} onClick={() => setDialogOpen(false)}>
191
+ <Card className="p-4 mb-4" id="graph-info-panel">
192
+ <h3 className="text-lg font-medium">Research Visualization</h3>
193
+ <p className="text-sm text-muted-foreground">Click on a node to see details. Source nodes are shown in purple. Click a node to open its source or view sources.</p>
194
+ </Card>
195
+ <div className="flex-1 border rounded-lg bg-card flex relative overflow-hidden" style={{ minHeight: 600 }}>
196
+ {!researchTree ? (
197
+ <div className="h-full w-full flex items-center justify-center text-muted-foreground">
198
+ <p>No research data available yet. Start a conversation to begin research.</p>
199
+ </div>
200
+ ) : (
201
+ <div style={{ width: "100%", height: "100%", position: "relative" }}>
202
+ {/* Background for interaction */}
203
+ <div style={{ position: "absolute", inset: 0, zIndex: 0, pointerEvents: "auto" }} onClick={() => setDialogOpen(false)} />
204
+ {/* SVG for links only */}
205
+ <svg width={dimensions.width} height={dimensions.height} style={{ position: "absolute", top: 0, left: 0, zIndex: 1, pointerEvents: "none" }}>
206
+ {(() => {
207
+ if (!researchTree) return null;
208
+ const nodes: GraphNode[] = [];
209
+ const links: GraphLink[] = [];
210
+ const processTree = (tree: ResearchTree, parentId?: string) => {
211
+ const nodeId = `${tree.query}_${tree.depth}`;
212
+ nodes.push({ id: nodeId, name: tree.query, size: 10, depth: tree.depth });
213
+ if (parentId) {
214
+ links.push({ source: parentId, target: nodeId });
215
+ }
216
+ tree.children.forEach((child) => processTree(child, nodeId));
217
+ };
218
+ processTree(researchTree);
219
+ return links.map((l, i) => {
220
+ const src = nodePositions[(typeof l.source === "string" ? l.source : l.source.id) as string];
221
+ const tgt = nodePositions[(typeof l.target === "string" ? l.target : l.target.id) as string];
222
+ if (!src || !tgt) return null;
223
+ return <line key={i} x1={src.x} y1={src.y} x2={tgt.x} y2={tgt.y} stroke="#bbb" strokeWidth={2} strokeOpacity={0.7} />;
224
+ });
225
+ })()}
226
+ </svg>
227
+ {/* Render node cards using nodesState */}
228
+ {nodesState.map(renderNodeCard)}
229
+ </div>
230
+ )}
231
+ </div>
232
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
233
+ <DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
234
+ <DialogHeader>
235
+ <DialogTitle className="text-xl font-semibold">Sources for: {selectedNodeContent.title}</DialogTitle>
236
+ </DialogHeader>
237
+ <div className="flex-1 overflow-y-auto py-4">{formatSourceContent()}</div>
238
+ </DialogContent>
239
+ </Dialog>
240
+ </div>
241
+ );
242
+ };
243
+
244
+ export default ResearchGraph;
frontend/src/lib/store/ChatContext.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client";
2
 
3
- import { ChatData, ChatState, Conversation, Message, ResearchOptions, ResearchResults, StatusUpdate } from "@/lib/types";
4
  import { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
5
  import { v4 as uuidv4 } from "uuid";
6
  import { disconnectSocket, getSocket, initializeSocket } from "@/lib/socket";
@@ -101,13 +101,9 @@ export const ChatProvider = ({ children }: { children: ReactNode }) => {
101
  if (conversation) {
102
  // Check if any loaded message has isProgress true
103
  let messages = conversation.messages;
104
- if (messages.some(msg => msg.isProgress === true)) {
105
  // Convert any progress messages to error messages
106
- messages = messages.map(msg =>
107
- msg.isProgress === true
108
- ? { ...msg, content: "Connection error", isProgress: false }
109
- : msg
110
- );
111
  }
112
 
113
  setChatState((prev) => ({ ...prev, messages, isLoading: false }));
@@ -128,17 +124,13 @@ export const ChatProvider = ({ children }: { children: ReactNode }) => {
128
 
129
  // When socket disconnects, update any progress messages to show connection error
130
  setChatState((prevState) => {
131
- const updatedMessages = prevState.messages.map(msg =>
132
- msg.isProgress === true
133
- ? { ...msg, content: "Connection error", isProgress: false }
134
- : msg
135
- );
136
 
137
  return {
138
  ...prevState,
139
  messages: updatedMessages,
140
  isLoading: false,
141
- error: "Lost connection to research server"
142
  };
143
  });
144
  });
@@ -153,15 +145,16 @@ export const ChatProvider = ({ children }: { children: ReactNode }) => {
153
  const lastProgressIndex = messages.findLastIndex((msg) => msg.role === "assistant" && msg.isProgress === true);
154
 
155
  if (lastProgressIndex !== -1) {
156
- // Update existing progress message
157
  messages[lastProgressIndex] = {
158
  ...messages[lastProgressIndex],
159
  content: progressText,
160
  progress: progress,
161
  timestamp: new Date(),
 
162
  };
163
  } else {
164
- // Add new progress message
165
  messages.push({
166
  id: uuidv4(),
167
  content: progressText,
@@ -169,6 +162,8 @@ export const ChatProvider = ({ children }: { children: ReactNode }) => {
169
  timestamp: new Date(),
170
  progress: progress,
171
  isProgress: true,
 
 
172
  });
173
  }
174
 
 
1
  "use client";
2
 
3
+ import { ChatData, ChatState, Conversation, Message, ResearchOptions, ResearchResults, ResearchTree, StatusUpdate } from "@/lib/types";
4
  import { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
5
  import { v4 as uuidv4 } from "uuid";
6
  import { disconnectSocket, getSocket, initializeSocket } from "@/lib/socket";
 
101
  if (conversation) {
102
  // Check if any loaded message has isProgress true
103
  let messages = conversation.messages;
104
+ if (messages.some((msg) => msg.isProgress === true)) {
105
  // Convert any progress messages to error messages
106
+ messages = messages.map((msg) => (msg.isProgress === true ? { ...msg, content: "Connection error", isProgress: false } : msg));
 
 
 
 
107
  }
108
 
109
  setChatState((prev) => ({ ...prev, messages, isLoading: false }));
 
124
 
125
  // When socket disconnects, update any progress messages to show connection error
126
  setChatState((prevState) => {
127
+ const updatedMessages = prevState.messages.map((msg) => (msg.isProgress === true ? { ...msg, content: "Connection error", isProgress: false } : msg));
 
 
 
 
128
 
129
  return {
130
  ...prevState,
131
  messages: updatedMessages,
132
  isLoading: false,
133
+ error: "Lost connection to research server",
134
  };
135
  });
136
  });
 
145
  const lastProgressIndex = messages.findLastIndex((msg) => msg.role === "assistant" && msg.isProgress === true);
146
 
147
  if (lastProgressIndex !== -1) {
148
+ // Update existing progress message with research_tree data
149
  messages[lastProgressIndex] = {
150
  ...messages[lastProgressIndex],
151
  content: progressText,
152
  progress: progress,
153
  timestamp: new Date(),
154
+ research_tree: data.research_tree, // Update the research_tree in real-time
155
  };
156
  } else {
157
+ // Add new progress message with research_tree
158
  messages.push({
159
  id: uuidv4(),
160
  content: progressText,
 
162
  timestamp: new Date(),
163
  progress: progress,
164
  isProgress: true,
165
+ research_tree: data.research_tree, // Include the research_tree
166
+ media: {}, // Initialize empty media object
167
  });
168
  }
169
 
frontend/src/lib/types.ts CHANGED
@@ -42,6 +42,7 @@ export interface Conversation {
42
  export interface StatusUpdate {
43
  message: string;
44
  progress: number;
 
45
  }
46
 
47
  // Simplified research results based on actual server output
@@ -49,22 +50,22 @@ export interface ResearchResults {
49
  topic: string;
50
  timestamp: string;
51
  // Optional fields not present in basic server response
52
- content?: string;
53
- media?: {
54
- images?: string[];
55
- videos?: string[];
56
- links?: Array<{
57
  text: string;
58
  url: string;
59
  }>;
60
  };
61
- research_tree?: ResearchTree;
62
  }
63
 
64
  export interface ResearchTree {
65
  query: string;
66
  depth: number;
67
- sources: string[];
68
  children: ResearchTree[];
69
  }
70
 
 
42
  export interface StatusUpdate {
43
  message: string;
44
  progress: number;
45
+ research_tree: ResearchTree;
46
  }
47
 
48
  // Simplified research results based on actual server output
 
50
  topic: string;
51
  timestamp: string;
52
  // Optional fields not present in basic server response
53
+ content: string;
54
+ media: {
55
+ images: string[];
56
+ videos: string[];
57
+ links: Array<{
58
  text: string;
59
  url: string;
60
  }>;
61
  };
62
+ research_tree: ResearchTree;
63
  }
64
 
65
  export interface ResearchTree {
66
  query: string;
67
  depth: number;
68
+ sources: Record<string, string>; // it's like { "https://...": "Webpage text..." }
69
  children: ResearchTree[];
70
  }
71