Soham Waghmare commited on
Commit
62283c0
·
1 Parent(s): 27a07a9

feat: better error and data handling

Browse files
backend/crawl_ai.py CHANGED
@@ -24,15 +24,14 @@ async def main(urls):
24
  semaphore_count=3,
25
  wait_for_images=True,
26
  )
27
- with open("output.json", "w") as f:
28
- f.write("")
29
  for result in results:
30
  if result.success:
31
  dump_result = {
32
  "url": result.url,
33
  "markdown": result.markdown,
34
  }
35
- with open("output.json", "a") as f:
36
  json.dump(dump_result, f)
37
  # Print the extracted content
38
  hr = lambda n=1: print(("-" * 80) * 2 * n)
 
24
  semaphore_count=3,
25
  wait_for_images=True,
26
  )
27
+ open("output.log.json", "w").close()
 
28
  for result in results:
29
  if result.success:
30
  dump_result = {
31
  "url": result.url,
32
  "markdown": result.markdown,
33
  }
34
+ with open("output.log.json", "a") as f:
35
  json.dump(dump_result, f)
36
  # Print the extracted content
37
  hr = lambda n=1: print(("-" * 80) * 2 * n)
backend/knet.py CHANGED
@@ -70,7 +70,7 @@ class Prompt:
70
  Return as JSON array of objects with properties:
71
  - query (string)""")
72
 
73
- self.report_outline = dedent("""Generate a comprehensive report outline on the user query based on the following research findings:
74
  User query:
75
  {topic}
76
 
@@ -79,7 +79,8 @@ class Prompt:
79
 
80
  The outline should include:
81
  - Title
82
- - List of h2 headings""")
 
83
 
84
  self.report_fillin = dedent("""Fill in the content for the following report outline on the user query based on the following research findings:
85
  User query:
@@ -88,11 +89,17 @@ class Prompt:
88
  Findings:
89
  {ctx_manager}
90
 
91
- Report Outline:
 
 
 
92
  {report_outline}
93
 
94
- Current slot to fill in: (h2 heading)
95
- {slot}
 
 
 
96
  """)
97
 
98
 
@@ -186,7 +193,9 @@ class KNet:
186
  try:
187
  # Generate research plan
188
  await self.progress.update(0, "Generating research plan...")
189
- self.research_plan = self.generate_content(self.prompt.research_plan.format(topic=topic), schema=self.schema.research_plan)["steps"]
 
 
190
  self.logger.info(f"Research plan:\n{json.dumps(self.research_plan, indent=2)}")
191
 
192
  master_node = ResearchNode()
@@ -201,11 +210,12 @@ class KNet:
201
  vertical=self.research_plan[self.idx_research_plan], research_plan="None", past_queries="None", ctx_manager="None", n=1
202
  ),
203
  schema=self.schema.search_query,
 
204
  )["branches"][0]
205
 
206
  root_node = ResearchNode(query)
207
- master_node.add_child(root_node)
208
- to_explore = deque([(root_node, 0)]) # (node, depth) pairs
209
  explored_queries = set() # {string, string, ...}
210
 
211
  await self.progress.update(100 / (len(self.research_plan) + 1), f"{self.research_plan[self.idx_research_plan]}")
@@ -234,7 +244,7 @@ class KNet:
234
 
235
  # Generate final report
236
  await self.progress.update(100 / (len(self.research_plan) + 1), "Generating final report...")
237
- final_report = self._generate_final_report(master_node, topic)
238
 
239
  self.logger.info(f"Research completed. Explored {len(explored_queries)} queries across {master_node.max_depth()} levels")
240
  await self.progress.update(100, "Research complete!")
@@ -247,9 +257,9 @@ class KNet:
247
  self.logger.error("Research failed", exc_info=True)
248
  raise
249
 
250
- def _generate_final_report(self, root_node: ResearchNode, topic: str, retry_count: int = 1) -> Dict[str, Any]:
251
  try:
252
- self.progress.setter(0, "Generating report...")
253
  findings = "\n\n------\n\n".join(self.ctx_manager)
254
  with open("ctx_manager.log.txt", "w", encoding="utf-8") as f:
255
  f.write(findings)
@@ -258,21 +268,26 @@ class KNet:
258
  outline = self.generate_content(self.prompt.report_outline.format(topic=topic, ctx_manager=findings), schema=self.schema.report_outline)
259
  self.logger.info(f"Report outline:\n{json.dumps(outline, indent=2)}")
260
  report = []
 
261
  # Fill in report outline
262
  for i, heading in enumerate(outline["headings"]):
263
- self.progress.update(100 / (len(outline["headings"]) + 1), "Generating report...")
264
  content = self.generate_content(
265
  self.prompt.report_fillin.format(
266
  topic=topic,
267
  ctx_manager=findings,
 
268
  report_outline=["[done] " + outline["title"]] + [f"[done] {h}" for _, h in enumerate(outline["headings"]) if i < _],
269
  slot=heading,
270
  ),
271
  schema=self.schema.report_fillin,
272
  )["content"]
 
 
 
 
273
  report.append({"heading": heading, "content": content})
274
- # Rasterize report
275
- raster_report = f"# {outline['title']}\n\n" + "\n\n".join([f"## {r['heading']}\n\n{r['content']}" for r in report])
276
 
277
  # Collate multimedia content
278
  media_content = {"images": [], "videos": [], "links": [], "references": []}
@@ -317,9 +332,11 @@ class KNet:
317
  }
318
 
319
  except Exception as e:
320
- if e in ["GEMINI_RECITATION", "NO_RESPONSE"] and retry_count < 3:
321
- self.logger.error(f"Retrying final report:C:{retry_count / 3}", exc_info=True)
322
- return self._generate_final_report(root_node, retry_count + 1)
 
 
323
  self.logger.error("Error generating final report", exc_info=True)
324
  raise
325
 
@@ -351,8 +368,10 @@ class KNet:
351
  return new_nodes
352
 
353
  except Exception as e:
354
- if e in ["GEMINI_RECITATION", "NO_RESPONSE"] and retry_count < 3:
355
- self.logger.error(f"Retrying _gen_queries | C:{retry_count / 3}", exc_info=True)
 
 
356
  return self._gen_queries(node, topic, retry_count + 1)
357
  self.logger.error("_gen_queries failed", exc_info=True)
358
  raise
@@ -381,13 +400,15 @@ class KNet:
381
  return response["decision"]
382
 
383
  except Exception as e:
384
- if e in ["GEMINI_RECITATION", "NO_RESPONSE"] and retry_count < 3:
385
- self.logger.error(f"Retrying branch decision:C:{retry_count / 3}", exc_info=True)
 
 
386
  return self._should_continue_branch(node, topic, retry_count + 1)
387
  self.logger.error("Branch decision failed:", exc_info=True)
388
  raise
389
 
390
- def generate_content(self, prompt: str, schema: Dict[str, Any] = {}, temp: float = 0.9) -> Dict[str, Any] | str:
391
  safe = [
392
  types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
393
  types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
 
70
  Return as JSON array of objects with properties:
71
  - query (string)""")
72
 
73
+ self.report_outline = dedent("""Generate a comprehensive outline for a report on the user query based on the findings:
74
  User query:
75
  {topic}
76
 
 
79
 
80
  The outline should include:
81
  - Title
82
+ - List of h2 headings
83
+ Do not include hashtags""")
84
 
85
  self.report_fillin = dedent("""Fill in the content for the following report outline on the user query based on the following research findings:
86
  User query:
 
89
  Findings:
90
  {ctx_manager}
91
 
92
+ Report generated os far:
93
+ {report_progress}
94
+
95
+ The outline:
96
  {report_outline}
97
 
98
+ Current heading to fill in:
99
+ ## {slot}
100
+
101
+ The content should be comprehensive, detailed and well-structured, providing detailed information on the topic. Use tables, lists, and other formatting as needed to enhance readability.
102
+ Do not include the heading in the content.
103
  """)
104
 
105
 
 
193
  try:
194
  # Generate research plan
195
  await self.progress.update(0, "Generating research plan...")
196
+ self.research_plan = self.generate_content(self.prompt.research_plan.format(topic=topic), schema=self.schema.research_plan, temp=1.5)[
197
+ "steps"
198
+ ]
199
  self.logger.info(f"Research plan:\n{json.dumps(self.research_plan, indent=2)}")
200
 
201
  master_node = ResearchNode()
 
210
  vertical=self.research_plan[self.idx_research_plan], research_plan="None", past_queries="None", ctx_manager="None", n=1
211
  ),
212
  schema=self.schema.search_query,
213
+ temp=1.5,
214
  )["branches"][0]
215
 
216
  root_node = ResearchNode(query)
217
+ master_node.add_child(root_node.query, node=root_node)
218
+ to_explore = deque([(root_node, 1)]) # (node, depth) pairs
219
  explored_queries = set() # {string, string, ...}
220
 
221
  await self.progress.update(100 / (len(self.research_plan) + 1), f"{self.research_plan[self.idx_research_plan]}")
 
244
 
245
  # Generate final report
246
  await self.progress.update(100 / (len(self.research_plan) + 1), "Generating final report...")
247
+ final_report = await self._generate_final_report(master_node, topic)
248
 
249
  self.logger.info(f"Research completed. Explored {len(explored_queries)} queries across {master_node.max_depth()} levels")
250
  await self.progress.update(100, "Research complete!")
 
257
  self.logger.error("Research failed", exc_info=True)
258
  raise
259
 
260
+ async def _generate_final_report(self, root_node: ResearchNode, topic: str, retry_count: int = 1) -> Dict[str, Any]:
261
  try:
262
+ await self.progress.setter(0, "Generating report...")
263
  findings = "\n\n------\n\n".join(self.ctx_manager)
264
  with open("ctx_manager.log.txt", "w", encoding="utf-8") as f:
265
  f.write(findings)
 
268
  outline = self.generate_content(self.prompt.report_outline.format(topic=topic, ctx_manager=findings), schema=self.schema.report_outline)
269
  self.logger.info(f"Report outline:\n{json.dumps(outline, indent=2)}")
270
  report = []
271
+ raster_report = f"# {outline['title']}\n\n"
272
  # Fill in report outline
273
  for i, heading in enumerate(outline["headings"]):
274
+ await self.progress.update(100 / (len(outline["headings"]) + 1), "Generating report...")
275
  content = self.generate_content(
276
  self.prompt.report_fillin.format(
277
  topic=topic,
278
  ctx_manager=findings,
279
+ report_progress=raster_report,
280
  report_outline=["[done] " + outline["title"]] + [f"[done] {h}" for _, h in enumerate(outline["headings"]) if i < _],
281
  slot=heading,
282
  ),
283
  schema=self.schema.report_fillin,
284
  )["content"]
285
+ # Remove heading if LLM put it there regardless
286
+ idx_heading = content.find(heading)
287
+ if idx_heading != -1:
288
+ content = content[idx_heading + len(heading) :].strip()
289
  report.append({"heading": heading, "content": content})
290
+ raster_report += f"\n\n## {heading}\n\n{content}"
 
291
 
292
  # Collate multimedia content
293
  media_content = {"images": [], "videos": [], "links": [], "references": []}
 
332
  }
333
 
334
  except Exception as e:
335
+ if e in ["GEMINI_RECITATION", "NO_RESPONSE"]:
336
+ self.logger.error("GEMINI_RECITATION or NO_RESPONSE")
337
+ if retry_count < 3:
338
+ self.logger.error(f"Retrying final report:C:{retry_count} / 3", exc_info=True)
339
+ return await self._generate_final_report(root_node, retry_count + 1)
340
  self.logger.error("Error generating final report", exc_info=True)
341
  raise
342
 
 
368
  return new_nodes
369
 
370
  except Exception as e:
371
+ if e in ["GEMINI_RECITATION", "NO_RESPONSE"]:
372
+ self.logger.error("GEMINI_RECITATION or NO_RESPONSE")
373
+ if retry_count < 3:
374
+ self.logger.error(f"Retrying _gen_queries | C:{retry_count} / 3", exc_info=True)
375
  return self._gen_queries(node, topic, retry_count + 1)
376
  self.logger.error("_gen_queries failed", exc_info=True)
377
  raise
 
400
  return response["decision"]
401
 
402
  except Exception as e:
403
+ if e in ["GEMINI_RECITATION", "NO_RESPONSE"]:
404
+ self.logger.error("GEMINI_RECITATION or NO_RESPONSE")
405
+ if retry_count < 3:
406
+ self.logger.error(f"Retrying branch decision:C:{retry_count} / 3", exc_info=True)
407
  return self._should_continue_branch(node, topic, retry_count + 1)
408
  self.logger.error("Branch decision failed:", exc_info=True)
409
  raise
410
 
411
+ def generate_content(self, prompt: str, schema: Dict[str, Any] = {}, temp: float = 1, _retry_count: int = 1) -> Dict[str, Any] | str:
412
  safe = [
413
  types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
414
  types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
backend/research_node.py CHANGED
@@ -10,8 +10,13 @@ class ResearchNode:
10
  self.children: List[ResearchNode] = []
11
  self.data: List[Dict[str, Any]] = []
12
 
13
- def add_child(self, query: str) -> "ResearchNode":
14
- child = ResearchNode(query, parent=self, depth=self.depth + 1)
 
 
 
 
 
15
  self.children.append(child)
16
  return child
17
 
 
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
17
+ child.depth = self.depth + 1
18
+ else:
19
+ child = ResearchNode(query, parent=self, depth=self.depth + 1)
20
  self.children.append(child)
21
  return child
22
 
backend/scraper.py CHANGED
@@ -163,7 +163,7 @@ class CrawlForAIScraper:
163
  headless=True,
164
  viewport_width=1920,
165
  viewport_height=1080,
166
- accept_downloads=True,
167
  verbose=False,
168
  )
169
  self.crawler = AsyncWebCrawler(config=self.base_browser)
@@ -185,19 +185,25 @@ class CrawlForAIScraper:
185
  self.logger.info(f"Querying: {query}")
186
 
187
  # Perform a search to get a list of webpages
188
- search_results = await self._search(query, num_sites)
189
 
190
  # Scrape each webpage
191
  scraped_data = []
192
- self.logger.info(f"Scraping {len(search_results)} sites...")
193
- data = await self._scrape_pages(search_results)
194
- if data:
195
- scraped_data.extend(data)
 
 
 
 
 
 
196
 
197
  self.logger.info(f"Completed scraping {len(scraped_data)} sites")
198
  return scraped_data
199
 
200
- async def _search(self, query: str, num_results: int) -> List[str]:
201
  try:
202
  encoded_query = quote_plus(query)
203
  search_uri = f"https://www.google.com/search?q={encoded_query}"
@@ -210,26 +216,28 @@ class CrawlForAIScraper:
210
  scan_full_page=True,
211
  )
212
 
213
- soup = BeautifulSoup(result.html, "html.parser")
214
  search_results = []
215
 
216
  for link in list(soup.select("div > span > a"))[2:]:
217
  url = link.get("href").replace(" ", "").replace("\n", "").strip()
218
  if not url.startswith(("http://", "https://")):
219
  url = "https://" + url
 
 
220
  search_results.append(url)
221
 
222
  self.logger.info(f"Found {len(search_results)} results")
223
- return search_results[:num_results]
224
 
225
  except requests.exceptions.RequestException as e:
226
- self.logger.error(f"Google search error: {str(e)}")
227
- return []
228
  except Exception as e:
229
- self.logger.error(f"Google search error: {str(e)}")
230
- return []
231
 
232
- async def _scrape_pages(self, urls: str) -> Dict[str, Any]:
233
  await self.start()
234
 
235
  try:
@@ -241,22 +249,47 @@ class CrawlForAIScraper:
241
  scan_full_page=True,
242
  semaphore_count=4,
243
  wait_for_images=True,
 
 
 
244
  page_timeout=25000,
245
  )
246
  scraped_sites = []
247
  for result in results:
248
  if result.success:
249
  soup = BeautifulSoup(result.html, "html.parser")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  data = {
251
  "url": result.url,
252
  "text": result.markdown,
253
- "images": self._extract_images(soup, result.url),
254
- "videos": self._extract_videos(soup),
255
- "links": result.links["external"],
256
  }
257
  scraped_sites.append(data)
258
  self.logger.info(f" - {result.url[:80]}...")
259
- return scraped_sites
260
 
261
  except Exception as e:
262
  self.logger.error(f"Scraping error while {urls}: {str(e)}")
@@ -293,20 +326,31 @@ class CrawlForAIScraper:
293
  videos = []
294
  nodes = list(soup.find_all("iframe")) + list(soup.find_all("video")) + list(soup.find_all("a"))
295
  for node in nodes:
296
- if node.name == "iframe":
297
- src = node.get("src", "")
298
- if "youtube.com" in src or "youtu.be" in src:
299
- videos.append(src)
300
- elif node.name == "video":
301
- src = node.get("src", "")
302
- if "youtube.com" in src or "youtu.be" in src:
303
- videos.append(src)
304
- elif node.name == "a":
305
- href = node.get("href", "")
306
- if "youtube.com" in href or "youtu.be" in href:
307
- videos.append(href)
308
  return videos
309
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
  if __name__ == "__main__":
312
  import sys
@@ -325,9 +369,9 @@ if __name__ == "__main__":
325
  async def main():
326
  scraper = CrawlForAIScraper()
327
  await scraper.start()
328
- data = await scraper.search_and_scrape("quantum computing")
329
  await scraper.close()
330
- with open("output.json", "w") as f:
331
  f.write(json.dumps(data, indent=2))
332
  print(json.dumps(data, indent=2))
333
 
 
163
  headless=True,
164
  viewport_width=1920,
165
  viewport_height=1080,
166
+ accept_downloads=False,
167
  verbose=False,
168
  )
169
  self.crawler = AsyncWebCrawler(config=self.base_browser)
 
185
  self.logger.info(f"Querying: {query}")
186
 
187
  # Perform a search to get a list of webpages
188
+ search_results = await self._search(query)
189
 
190
  # Scrape each webpage
191
  scraped_data = []
192
+ self.logger.info(f"Scraping {num_sites} sites...")
193
+ data = await self._scrape_pages(search_results[: num_sites + 2], num_sites)
194
+ scraped_data.extend(data)
195
+
196
+ # Scrape next pages when some failed
197
+ for _ in range(3):
198
+ if len(scraped_data) < num_sites:
199
+ idx_last_page = search_results.index(search_results[-1])
200
+ data = await self._scrape_pages(search_results[idx_last_page + 1 : num_sites + 2], num_sites)
201
+ scraped_data.extend(data)
202
 
203
  self.logger.info(f"Completed scraping {len(scraped_data)} sites")
204
  return scraped_data
205
 
206
+ async def _search(self, query: str) -> List[str]:
207
  try:
208
  encoded_query = quote_plus(query)
209
  search_uri = f"https://www.google.com/search?q={encoded_query}"
 
216
  scan_full_page=True,
217
  )
218
 
219
+ soup = BeautifulSoup(result.cleaned_html, "html.parser")
220
  search_results = []
221
 
222
  for link in list(soup.select("div > span > a"))[2:]:
223
  url = link.get("href").replace(" ", "").replace("\n", "").strip()
224
  if not url.startswith(("http://", "https://")):
225
  url = "https://" + url
226
+ if "support.google.com" in url or url.startswith("/search?q="):
227
+ continue
228
  search_results.append(url)
229
 
230
  self.logger.info(f"Found {len(search_results)} results")
231
+ return search_results
232
 
233
  except requests.exceptions.RequestException as e:
234
+ self.logger.error(f"Google search error: {str(e)}", exc_info=True)
235
+ raise
236
  except Exception as e:
237
+ self.logger.error(f"Google search error: {str(e)}", exc_info=True)
238
+ raise
239
 
240
+ async def _scrape_pages(self, urls: str, max_sites: int) -> Dict[str, Any]:
241
  await self.start()
242
 
243
  try:
 
249
  scan_full_page=True,
250
  semaphore_count=4,
251
  wait_for_images=True,
252
+ scroll_delay=0.1,
253
+ delay_before_return_html=2,
254
+ exclude_external_images=True,
255
  page_timeout=25000,
256
  )
257
  scraped_sites = []
258
  for result in results:
259
  if result.success:
260
  soup = BeautifulSoup(result.html, "html.parser")
261
+
262
+ # Combine images
263
+ extracted_images = self._extract_images(soup, result.url)
264
+ media_images = []
265
+ for img in result.media["images"]:
266
+ if img["width"] is None or (isinstance(img["width"], (int, float)) and img["width"] > 300):
267
+ # Resolve multiple URLs in the src attribute
268
+ src = img["src"]
269
+ if " " in src and "w," in src:
270
+ urls = [url.strip() for url in src.split(" ") if url.strip()]
271
+ if urls:
272
+ last_url = urls[-1].split(" ")[0]
273
+ media_images.append(last_url)
274
+ else:
275
+ media_images.append(src)
276
+ all_images = list(set(extracted_images + media_images))
277
+
278
+ # Combine videos
279
+ all_videos = self._extract_videos(soup)
280
+ media_videos = [v["src"] for v in result.media["videos"] if v["src"]]
281
+ all_videos = list(set(all_videos + media_videos))
282
+
283
  data = {
284
  "url": result.url,
285
  "text": result.markdown,
286
+ "images": all_images,
287
+ "videos": all_videos,
288
+ "links": self._extract_links(result.links["external"]),
289
  }
290
  scraped_sites.append(data)
291
  self.logger.info(f" - {result.url[:80]}...")
292
+ return scraped_sites[: max_sites]
293
 
294
  except Exception as e:
295
  self.logger.error(f"Scraping error while {urls}: {str(e)}")
 
326
  videos = []
327
  nodes = list(soup.find_all("iframe")) + list(soup.find_all("video")) + list(soup.find_all("a"))
328
  for node in nodes:
329
+ if not any(
330
+ keyword in node.get("src", "") or keyword in node.get("href", "")
331
+ for keyword in ["accounts.google.com", "blob:", "youtube.com/redirect"]
332
+ ):
333
+ continue
334
+ elif (
335
+ any(node.name in tag for tag in ["video", "iframe", "a"])
336
+ and "www.youtube.com/watch?v" in node.get("src", "")
337
+ or "www.youtube.com/watch?v" in node.get("href", "")
338
+ ):
339
+ videos.append(node.get("src", ""))
 
340
  return videos
341
 
342
+ def _extract_links(self, links: list) -> List[str]:
343
+ # Filter out unwanted links
344
+ filtered_links = []
345
+ for link in links:
346
+ url = link.get("href")
347
+ if url.startswith(("http://", "https://")) and not any(
348
+ keyword in url
349
+ for keyword in ["support.google.com", "google.com", "accounts.google.com", "youtube.com", "blob:", "mailto:", "javascript:"]
350
+ ):
351
+ filtered_links.append(link)
352
+ return filtered_links
353
+
354
 
355
  if __name__ == "__main__":
356
  import sys
 
369
  async def main():
370
  scraper = CrawlForAIScraper()
371
  await scraper.start()
372
+ data = await scraper.search_and_scrape("blender.org")
373
  await scraper.close()
374
+ with open("output.log.json", "w") as f:
375
  f.write(json.dumps(data, indent=2))
376
  print(json.dumps(data, indent=2))
377
 
frontend/bun.lock CHANGED
@@ -15,6 +15,7 @@
15
  "@radix-ui/react-slot": "^1.1.2",
16
  "@radix-ui/react-tabs": "^1.1.3",
17
  "@radix-ui/react-tooltip": "^1.1.8",
 
18
  "class-variance-authority": "^0.7.1",
19
  "clsx": "^2.1.1",
20
  "eslint-config-next": "^15.2.4",
@@ -25,6 +26,7 @@
25
  "react-dom": "^19.1.0",
26
  "react-markdown": "^10.1.0",
27
  "react-resizable-panels": "^2.1.7",
 
28
  "remark-gfm": "^4.0.1",
29
  "shadcn": "^2.4.0-canary.19",
30
  "socket.io-client": "^4.8.1",
@@ -92,6 +94,8 @@
92
 
93
  "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.26.8", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw=="],
94
 
 
 
95
  "@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="],
96
 
97
  "@babel/traverse": ["@babel/traverse@7.26.10", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A=="],
@@ -356,6 +360,8 @@
356
 
357
  "@types/react-dom": ["@types/react-dom@18.3.5", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q=="],
358
 
 
 
359
  "@types/statuses": ["@types/statuses@2.0.5", "", {}, "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A=="],
360
 
361
  "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
@@ -502,13 +508,13 @@
502
 
503
  "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
504
 
505
- "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
506
 
507
  "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
508
 
509
- "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
510
 
511
- "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
512
 
513
  "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
514
 
@@ -680,6 +686,8 @@
680
 
681
  "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
682
 
 
 
683
  "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
684
 
685
  "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
@@ -698,6 +706,8 @@
698
 
699
  "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
700
 
 
 
701
  "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
702
 
703
  "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
@@ -758,12 +768,20 @@
758
 
759
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
760
 
 
 
761
  "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
762
 
763
  "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
764
 
 
 
765
  "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
766
 
 
 
 
 
767
  "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
768
 
769
  "https-proxy-agent": ["https-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA=="],
@@ -784,9 +802,9 @@
784
 
785
  "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=="],
786
 
787
- "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
788
 
789
- "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
790
 
791
  "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
792
 
@@ -810,7 +828,7 @@
810
 
811
  "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
812
 
813
- "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
814
 
815
  "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
816
 
@@ -822,7 +840,7 @@
822
 
823
  "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
824
 
825
- "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
826
 
827
  "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
828
 
@@ -916,6 +934,8 @@
916
 
917
  "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
918
 
 
 
919
  "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
920
 
921
  "lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="],
@@ -1088,7 +1108,7 @@
1088
 
1089
  "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
1090
 
1091
- "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
1092
 
1093
  "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
1094
 
@@ -1132,6 +1152,8 @@
1132
 
1133
  "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
1134
 
 
 
1135
  "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
1136
 
1137
  "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
@@ -1162,6 +1184,8 @@
1162
 
1163
  "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
1164
 
 
 
1165
  "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
1166
 
1167
  "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
@@ -1172,6 +1196,10 @@
1172
 
1173
  "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
1174
 
 
 
 
 
1175
  "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
1176
 
1177
  "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
@@ -1410,6 +1438,8 @@
1410
 
1411
  "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
1412
 
 
 
1413
  "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
1414
 
1415
  "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
@@ -1466,6 +1496,8 @@
1466
 
1467
  "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=="],
1468
 
 
 
1469
  "eslint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
1470
 
1471
  "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -1486,24 +1518,34 @@
1486
 
1487
  "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1488
 
 
 
 
 
 
 
 
 
1489
  "is-bun-module/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
1490
 
1491
  "log-symbols/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
1492
 
1493
  "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
1494
 
 
 
1495
  "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
1496
 
1497
  "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
1498
 
1499
  "ora/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
1500
 
1501
- "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
1502
-
1503
  "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
1504
 
1505
  "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
1506
 
 
 
1507
  "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
1508
 
1509
  "sharp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@@ -1518,6 +1560,8 @@
1518
 
1519
  "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1520
 
 
 
1521
  "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1522
 
1523
  "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
@@ -1546,6 +1590,20 @@
1546
 
1547
  "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
1548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1549
  "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
1550
 
1551
  "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -1555,5 +1613,7 @@
1555
  "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1556
 
1557
  "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 
 
1558
  }
1559
  }
 
15
  "@radix-ui/react-slot": "^1.1.2",
16
  "@radix-ui/react-tabs": "^1.1.3",
17
  "@radix-ui/react-tooltip": "^1.1.8",
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",
 
26
  "react-dom": "^19.1.0",
27
  "react-markdown": "^10.1.0",
28
  "react-resizable-panels": "^2.1.7",
29
+ "react-syntax-highlighter": "^15.6.1",
30
  "remark-gfm": "^4.0.1",
31
  "shadcn": "^2.4.0-canary.19",
32
  "socket.io-client": "^4.8.1",
 
94
 
95
  "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.26.8", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw=="],
96
 
97
+ "@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="],
98
+
99
  "@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="],
100
 
101
  "@babel/traverse": ["@babel/traverse@7.26.10", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A=="],
 
360
 
361
  "@types/react-dom": ["@types/react-dom@18.3.5", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q=="],
362
 
363
+ "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
364
+
365
  "@types/statuses": ["@types/statuses@2.0.5", "", {}, "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A=="],
366
 
367
  "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
 
508
 
509
  "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
510
 
511
+ "character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="],
512
 
513
  "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
514
 
515
+ "character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="],
516
 
517
+ "character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="],
518
 
519
  "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
520
 
 
686
 
687
  "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
688
 
689
+ "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="],
690
+
691
  "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
692
 
693
  "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
 
706
 
707
  "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
708
 
709
+ "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
710
+
711
  "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
712
 
713
  "fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
 
768
 
769
  "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
770
 
771
+ "hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="],
772
+
773
  "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
774
 
775
  "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
776
 
777
+ "hastscript": ["hastscript@6.0.0", "", { "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" } }, "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w=="],
778
+
779
  "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
780
 
781
+ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="],
782
+
783
+ "highlightjs-vue": ["highlightjs-vue@1.0.0", "", {}, "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA=="],
784
+
785
  "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
786
 
787
  "https-proxy-agent": ["https-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^7.0.2", "debug": "4" } }, "sha512-ONsE3+yfZF2caH5+bJlcddtWqNI3Gvs5A38+ngvljxaBiRXRswym2c7yf8UAeFpRFKjFNHIFEHqR/OLAWJzyiA=="],
 
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=="],
808
 
809
  "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
810
 
 
828
 
829
  "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
830
 
831
+ "is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="],
832
 
833
  "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
834
 
 
840
 
841
  "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
842
 
843
+ "is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="],
844
 
845
  "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
846
 
 
934
 
935
  "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
936
 
937
+ "lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="],
938
+
939
  "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
940
 
941
  "lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="],
 
1108
 
1109
  "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
1110
 
1111
+ "parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="],
1112
 
1113
  "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
1114
 
 
1152
 
1153
  "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
1154
 
1155
+ "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
1156
+
1157
  "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
1158
 
1159
  "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
 
1184
 
1185
  "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
1186
 
1187
+ "react-syntax-highlighter": ["react-syntax-highlighter@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg=="],
1188
+
1189
  "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
1190
 
1191
  "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
 
1196
 
1197
  "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
1198
 
1199
+ "refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="],
1200
+
1201
+ "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
1202
+
1203
  "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
1204
 
1205
  "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
 
1438
 
1439
  "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
1440
 
1441
+ "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
1442
+
1443
  "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
1444
 
1445
  "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
 
1496
 
1497
  "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=="],
1498
 
1499
+ "decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
1500
+
1501
  "eslint/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
1502
 
1503
  "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
 
1518
 
1519
  "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1520
 
1521
+ "hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="],
1522
+
1523
+ "hastscript/comma-separated-tokens": ["comma-separated-tokens@1.0.8", "", {}, "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw=="],
1524
+
1525
+ "hastscript/property-information": ["property-information@5.6.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA=="],
1526
+
1527
+ "hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="],
1528
+
1529
  "is-bun-module/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
1530
 
1531
  "log-symbols/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
1532
 
1533
  "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
1534
 
1535
+ "mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
1536
+
1537
  "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
1538
 
1539
  "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
1540
 
1541
  "ora/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
1542
 
 
 
1543
  "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
1544
 
1545
  "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
1546
 
1547
+ "refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
1548
+
1549
  "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
1550
 
1551
  "sharp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
 
1560
 
1561
  "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1562
 
1563
+ "stringify-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
1564
+
1565
  "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1566
 
1567
  "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
 
1590
 
1591
  "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
1592
 
1593
+ "hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
1594
+
1595
+ "mdast-util-mdx-jsx/parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
1596
+
1597
+ "mdast-util-mdx-jsx/parse-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
1598
+
1599
+ "mdast-util-mdx-jsx/parse-entities/character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
1600
+
1601
+ "mdast-util-mdx-jsx/parse-entities/is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
1602
+
1603
+ "mdast-util-mdx-jsx/parse-entities/is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
1604
+
1605
+ "mdast-util-mdx-jsx/parse-entities/is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
1606
+
1607
  "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
1608
 
1609
  "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 
1613
  "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1614
 
1615
  "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1616
+
1617
+ "mdast-util-mdx-jsx/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
1618
  }
1619
  }
frontend/package.json CHANGED
@@ -20,6 +20,7 @@
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
  "eslint-config-next": "^15.2.4",
@@ -30,6 +31,7 @@
30
  "react-dom": "^19.1.0",
31
  "react-markdown": "^10.1.0",
32
  "react-resizable-panels": "^2.1.7",
 
33
  "remark-gfm": "^4.0.1",
34
  "shadcn": "^2.4.0-canary.19",
35
  "socket.io-client": "^4.8.1",
 
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
+ "@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",
 
31
  "react-dom": "^19.1.0",
32
  "react-markdown": "^10.1.0",
33
  "react-resizable-panels": "^2.1.7",
34
+ "react-syntax-highlighter": "^15.6.1",
35
  "remark-gfm": "^4.0.1",
36
  "shadcn": "^2.4.0-canary.19",
37
  "socket.io-client": "^4.8.1",
frontend/src/components/ChatInterface.tsx CHANGED
@@ -27,7 +27,18 @@ const loadFromStorage = (): ChatData => {
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) {
@@ -45,7 +56,7 @@ const ChatInterface = () => {
45
  sources: true,
46
  citations: false,
47
  max_depth: 1,
48
- num_sites_per_query: 5,
49
  });
50
 
51
  const userInputRef = useRef<HTMLTextAreaElement>(null);
@@ -118,7 +129,10 @@ const ChatInterface = () => {
118
  // Format research stats and response
119
  const stats = [`Total Queries: ${results.metadata.total_queries}`, `Sources Used: ${results.metadata.total_sources}`, `Search Depth: ${results.metadata.max_depth_reached}`].join(" | ");
120
 
121
- 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("");
 
 
 
122
 
123
  const newMessages = [
124
  ...messages,
@@ -127,6 +141,8 @@ const ChatInterface = () => {
127
  content: formattedResponse,
128
  role: "assistant" as const,
129
  timestamp: new Date(results.timestamp),
 
 
130
  },
131
  ];
132
 
@@ -172,9 +188,9 @@ const ChatInterface = () => {
172
  setCurrentConversationId(data.currentConversationId);
173
 
174
  if (data.currentConversationId) {
175
- const currentConv = data.conversations.find((c) => c.id === data.currentConversationId);
176
- if (currentConv) {
177
- setChatState((prev) => ({ ...prev, messages: currentConv.messages }));
178
  }
179
  }
180
  }, []);
@@ -257,7 +273,7 @@ const ChatInterface = () => {
257
  const handleNewConversation = () => {
258
  userInputRef.current?.focus();
259
  setCurrentConversationId(null);
260
- setChatState((prev) => ({
261
  messages: [],
262
  isLoading: false,
263
  error: null,
 
27
  try {
28
  const parsed = JSON.parse(data);
29
  return {
30
+ conversations: Array.isArray(parsed.conversations)
31
+ ? parsed.conversations.map((conv: Conversation) => ({
32
+ ...conv,
33
+ messages: Array.isArray(conv.messages)
34
+ ? conv.messages.map((msg: Message) => ({
35
+ ...msg,
36
+ // Ensure media property is preserved if it exists
37
+ media: msg.media || undefined,
38
+ }))
39
+ : [],
40
+ }))
41
+ : [],
42
  currentConversationId: parsed.currentConversationId,
43
  };
44
  } catch (e) {
 
56
  sources: true,
57
  citations: false,
58
  max_depth: 1,
59
+ num_sites_per_query: 3,
60
  });
61
 
62
  const userInputRef = useRef<HTMLTextAreaElement>(null);
 
129
  // Format research stats and response
130
  const stats = [`Total Queries: ${results.metadata.total_queries}`, `Sources Used: ${results.metadata.total_sources}`, `Search Depth: ${results.metadata.max_depth_reached}`].join(" | ");
131
 
132
+ // Format images in a way the Message component can extract
133
+ const imageMarkdown = results.media?.images?.length ? `\n\n**Relevant Images:**\n${results.media.images.map((img) => `![Image](${img})`).join("\n")}` : "";
134
+
135
+ const formattedResponse = [results.content, `\n\n---\n**Research Stats:**\n${stats}`, imageMarkdown].join("");
136
 
137
  const newMessages = [
138
  ...messages,
 
141
  content: formattedResponse,
142
  role: "assistant" as const,
143
  timestamp: new Date(results.timestamp),
144
+ // Store the media object directly with the message
145
+ media: results.media,
146
  },
147
  ];
148
 
 
188
  setCurrentConversationId(data.currentConversationId);
189
 
190
  if (data.currentConversationId) {
191
+ const conversation = data.conversations.find((c) => c.id === data.currentConversationId);
192
+ if (conversation) {
193
+ setChatState((prev) => ({ ...prev, messages: conversation.messages }));
194
  }
195
  }
196
  }, []);
 
273
  const handleNewConversation = () => {
274
  userInputRef.current?.focus();
275
  setCurrentConversationId(null);
276
+ setChatState(() => ({
277
  messages: [],
278
  isLoading: false,
279
  error: null,
frontend/src/components/Message.tsx CHANGED
@@ -5,9 +5,69 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
5
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
6
  import { Message as MessageType } from "@/lib/types";
7
  import { Bot, Copy, MoreHorizontal, User2 } from "lucide-react";
8
- import React from "react";
9
  import ReactMarkdown from "react-markdown";
10
  import remarkGfm from "remark-gfm";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  const MarkdownComponents: Record<string, React.ComponentType<any>> = {
13
  h1: ({ children }) => <h1 className="text-2xl font-bold mb-4">{children}</h1>,
@@ -17,27 +77,83 @@ const MarkdownComponents: Record<string, React.ComponentType<any>> = {
17
  ul: ({ children }) => <ul className="list-disc ml-6 mb-4">{children}</ul>,
18
  ol: ({ children }) => <ol className="list-decimal ml-6 mb-4">{children}</ol>,
19
  li: ({ children }) => <li className="mb-1">{children}</li>,
20
- code: ({ node, inline, className, children, ...props }) => (
21
- // <ScrollArea asChild >
22
- <code className={`${Object.keys(node.properties).length === 0 ? "bg-muted px-1 py-0.5 rounded-md text-sm" : "block bg-muted p-1 rounded-lg text-sm overflow-x-auto max-w-full mb-1"}`} {...props}>
 
 
 
23
  {children}
24
  </code>
25
- // </ScrollArea>
26
- ),
27
- pre: ({ children }) => <pre className="bg-transparent p-0 max-w-full overflow-x-auto">{children}</pre>,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  a: ({ children, href }) => (
29
  <a href={href} className="text-primary hover:underline" target="_blank" rel="noopener noreferrer">
30
  {children}
31
  </a>
32
  ),
33
  blockquote: ({ children }) => <blockquote className="border-l-4 border-border pl-4 italic my-4">{children}</blockquote>,
34
- table: ({ children }) => <table className="w-full border-collapse my-4 border-slate-300 dark:border-slate-200">{children}</table>,
35
- thead: ({ children }) => <thead className="bg-muted/50">{children}</thead>,
 
 
 
 
 
 
 
 
36
  };
37
 
38
  const Message = ({ message }: { message: MessageType }) => {
39
  const isUser = message.role === "user";
40
  const isProgress = message.content.includes("%)") && message.role === "assistant";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  const copyToClipboard = () => {
43
  navigator.clipboard.writeText(message.content);
@@ -85,8 +201,10 @@ const Message = ({ message }: { message: MessageType }) => {
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>
 
5
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
6
  import { Message as MessageType } from "@/lib/types";
7
  import { Bot, Copy, MoreHorizontal, User2 } from "lucide-react";
8
+ import React, { useState, useEffect } from "react";
9
  import ReactMarkdown from "react-markdown";
10
  import remarkGfm from "remark-gfm";
11
+ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
12
+ import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
13
+
14
+ // ImageGallery component for handling images in a scrollable container
15
+ const ImageGallery = ({ imageUrls }: { imageUrls: string[] }) => {
16
+ const [loadedImages, setLoadedImages] = useState<string[]>([]);
17
+
18
+ // Lazy load images using Intersection Observer
19
+ useEffect(() => {
20
+ const observer = new IntersectionObserver(
21
+ (entries) => {
22
+ entries.forEach((entry) => {
23
+ if (entry.isIntersecting) {
24
+ const img = entry.target as HTMLImageElement;
25
+ const src = img.getAttribute("data-src");
26
+ if (src) {
27
+ img.src = src;
28
+ observer.unobserve(img);
29
+ setLoadedImages((prev) => [...prev, src]);
30
+ }
31
+ }
32
+ });
33
+ },
34
+ { rootMargin: "100px" }
35
+ );
36
+
37
+ const imgPlaceholders = document.querySelectorAll(".lazy-image");
38
+ imgPlaceholders.forEach((img) => observer.observe(img));
39
+
40
+ return () => observer.disconnect();
41
+ }, [imageUrls]);
42
+
43
+ if (!imageUrls || imageUrls.length === 0) return null;
44
+
45
+ return (
46
+ <div className="mt-4 mb-4">
47
+ <h3 className="text-md font-semibold mb-2">Relevant Images:</h3>
48
+ <ScrollArea className="w-full max-h-72 rounded-md border">
49
+ <div className="p-2 space-y-3">
50
+ {imageUrls.map((url, index) => (
51
+ <div key={index} className="image-container">
52
+ <img
53
+ className="lazy-image rounded-md max-w-full"
54
+ src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23f1f1f1'/%3E%3C/svg%3E"
55
+ data-src={url}
56
+ alt={`Research image ${index + 1}`}
57
+ loading="lazy"
58
+ onError={(e) => {
59
+ const target = e.target as HTMLImageElement;
60
+ target.onerror = null;
61
+ target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23f1f1f1'/%3E%3Ctext x='50%' y='50%' font-size='12' text-anchor='middle' alignment-baseline='middle' font-family='Arial, sans-serif'%3EImage failed to load%3C/text%3E%3C/svg%3E";
62
+ }}
63
+ />
64
+ </div>
65
+ ))}
66
+ </div>
67
+ </ScrollArea>
68
+ </div>
69
+ );
70
+ };
71
 
72
  const MarkdownComponents: Record<string, React.ComponentType<any>> = {
73
  h1: ({ children }) => <h1 className="text-2xl font-bold mb-4">{children}</h1>,
 
77
  ul: ({ children }) => <ul className="list-disc ml-6 mb-4">{children}</ul>,
78
  ol: ({ children }) => <ol className="list-decimal ml-6 mb-4">{children}</ol>,
79
  li: ({ children }) => <li className="mb-1">{children}</li>,
80
+ code: ({ node, inline, className, children, ...props }) => {
81
+ const match = /language-(\w+)/.exec(className || "");
82
+ const language = match ? match[1] : "";
83
+
84
+ return Object.keys(node.properties).length === 0 ? (
85
+ <code className="bg-muted px-1 py-0.5 rounded-md text-sm" {...props}>
86
  {children}
87
  </code>
88
+ ) : (
89
+ <ScrollArea className="w-full max-w-full">
90
+ <SyntaxHighlighter
91
+ style={oneDark}
92
+ language={language || "text"}
93
+ PreTag="div"
94
+ className="rounded-md my-2 text-sm"
95
+ showLineNumbers
96
+ customStyle={{
97
+ margin: 0,
98
+ borderRadius: "0.5rem",
99
+ padding: "1rem",
100
+ }}
101
+ {...props}>
102
+ {String(children).replace(/\n$/, "")}
103
+ </SyntaxHighlighter>
104
+ </ScrollArea>
105
+ );
106
+ },
107
+ pre: ({ children }) => <div className="bg-transparent p-0 max-w-full">{children}</div>,
108
  a: ({ children, href }) => (
109
  <a href={href} className="text-primary hover:underline" target="_blank" rel="noopener noreferrer">
110
  {children}
111
  </a>
112
  ),
113
  blockquote: ({ children }) => <blockquote className="border-l-4 border-border pl-4 italic my-4">{children}</blockquote>,
114
+ table: ({ children }) => (
115
+ <div className="overflow-x-auto border rounded-2xl my-4">
116
+ <table className="w-full border-collapse rounded-2xl overflow-hidden shadow-sm">{children}</table>
117
+ </div>
118
+ ),
119
+ thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
120
+ tbody: ({ children }) => <tbody>{children}</tbody>,
121
+ th: ({ children }) => <th className="border-r last:border-r-0 border-slate-900 px-4 py-2 text-left font-semibold">{children}</th>,
122
+ tr: ({ children }) => <tr className="border-b last:border-b-0 border-border">{children}</tr>,
123
+ td: ({ children }) => <td className="border-r last:border-r-0 border-border px-4 py-2">{children}</td>,
124
  };
125
 
126
  const Message = ({ message }: { message: MessageType }) => {
127
  const isUser = message.role === "user";
128
  const isProgress = message.content.includes("%)") && message.role === "assistant";
129
+ const [imageUrls, setImageUrls] = useState<string[]>([]);
130
+
131
+ // Extract image URLs from the message content or use the media object
132
+ useEffect(() => {
133
+ if (!isUser && !isProgress) {
134
+ let urls: string[] = [];
135
+
136
+ // First, check if there's a media object with images
137
+ if (message.media?.images && message.media.images.length > 0) {
138
+ urls = message.media.images;
139
+ } else {
140
+ // Fallback to extracting from markdown content
141
+ const imgRegex = /!\[.*?\]\((.*?)\)/g;
142
+ let match;
143
+
144
+ while ((match = imgRegex.exec(message.content)) !== null) {
145
+ if (match[1]) {
146
+ urls.push(match[1]);
147
+ }
148
+ }
149
+ }
150
+
151
+ setImageUrls(urls);
152
+ }
153
+ }, [message.content, message.media, isUser, isProgress]);
154
+
155
+ // Prepare message content without image markdown
156
+ const cleanContent = message.content.replace(/!\[.*?\]\(.*?\)\n?/g, "").replace(/\*\*Relevant Images:\*\*\n/g, "");
157
 
158
  const copyToClipboard = () => {
159
  navigator.clipboard.writeText(message.content);
 
201
 
202
  <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" }}>
203
  <ReactMarkdown remarkPlugins={[remarkGfm]} components={MarkdownComponents}>
204
+ {cleanContent}
205
  </ReactMarkdown>
206
+
207
+ {imageUrls.length > 0 && <ImageGallery imageUrls={imageUrls} />}
208
  </div>
209
  </div>
210
  </div>
frontend/src/components/ui/ChatLayout.tsx CHANGED
@@ -3,11 +3,12 @@ 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, 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
 
12
  interface ChatLayoutProps {
13
  sidebar: React.ReactNode;
@@ -19,8 +20,28 @@ const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsP
19
  return (
20
  <div className="h-screen flex flex-col">
21
  <header className="border-b-2 h-14 flex items-center px-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  <Link href={"/"}>
23
- <h1 className="text-xl font-semibold">KnowledgeNet: Deep Research</h1>
 
24
  </Link>
25
  <div className="flex-1" />
26
  <ThemeToggle />
@@ -33,9 +54,7 @@ const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsP
33
  <DialogContent>
34
  <DialogHeader>
35
  <DialogTitle>Research Settings</DialogTitle>
36
- <DialogDescription>
37
- Configure your research parameters and preferences.
38
- </DialogDescription>
39
  </DialogHeader>
40
  {settingsPanel}
41
  </DialogContent>
@@ -52,7 +71,7 @@ const ChatLayout: React.FC<ChatLayoutProps> = ({ sidebar, mainContent, settingsP
52
 
53
  <ResizableHandle withHandle className="hidden md:flex" />
54
 
55
- <ResizablePanel defaultSize={75}>
56
  <Tabs defaultValue="chat" className="h-full flex flex-col">
57
  <div className="p-4">
58
  <TabsList className="">
 
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;
 
20
  return (
21
  <div className="h-screen flex flex-col">
22
  <header className="border-b-2 h-14 flex items-center px-6">
23
+ <Sheet>
24
+ <SheetTrigger asChild>
25
+ <Button variant="ghost" size="icon" className="md:hidden mr-2">
26
+ <Menu size={20} />
27
+ <span className="sr-only">Toggle sidebar</span>
28
+ </Button>
29
+ </SheetTrigger>
30
+ <SheetContent side="left" className="w-[80%] sm:w-[350px] p-0">
31
+ <SheetTitle className="sr-only">Mobile Navigation</SheetTitle>
32
+ <SheetDescription className="sr-only">
33
+ Sidebar navigation for mobile devices
34
+ </SheetDescription>
35
+ <div className="border-b p-4">
36
+ <h2 className="text-lg font-semibold">Conversations</h2>
37
+ </div>
38
+ <ScrollArea className="h-[calc(100%-60px)] py-2">{sidebar}</ScrollArea>
39
+ </SheetContent>
40
+ </Sheet>
41
+
42
  <Link href={"/"}>
43
+ <h1 className="text-xl font-semibold hidden sm:inline">KnowledgeNet: Deep Research</h1>
44
+ <h1 className="text-lg font-semibold sm:hidden">KNet: Deep Research</h1>
45
  </Link>
46
  <div className="flex-1" />
47
  <ThemeToggle />
 
54
  <DialogContent>
55
  <DialogHeader>
56
  <DialogTitle>Research Settings</DialogTitle>
57
+ <DialogDescription>Configure your research parameters and preferences.</DialogDescription>
 
 
58
  </DialogHeader>
59
  {settingsPanel}
60
  </DialogContent>
 
71
 
72
  <ResizableHandle withHandle className="hidden md:flex" />
73
 
74
+ <ResizablePanel defaultSize={75} className="w-full md:w-auto">
75
  <Tabs defaultValue="chat" className="h-full flex flex-col">
76
  <div className="p-4">
77
  <TabsList className="">
frontend/src/components/ui/dialog.tsx CHANGED
@@ -41,6 +41,7 @@ const DialogContent = React.forwardRef<
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}
 
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
+ aria-describedby={!props['aria-describedby'] ? undefined : props['aria-describedby']}
45
  {...props}
46
  >
47
  {children}
frontend/src/components/ui/sheet.tsx CHANGED
@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
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}
@@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
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: {
@@ -40,7 +40,7 @@ const sheetVariants = cva(
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: {
@@ -62,13 +62,14 @@ const SheetContent = React.forwardRef<
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
  ))
 
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}
 
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=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35
  {
36
  variants: {
37
  side: {
 
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: {
 
62
  <SheetPrimitive.Content
63
  ref={ref}
64
  className={cn(sheetVariants({ side }), className)}
65
+ aria-describedby={!props['aria-describedby'] ? undefined : props['aria-describedby']}
66
  {...props}
67
  >
68
+ {children}
69
  <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">
70
  <X className="h-4 w-4" />
71
  <span className="sr-only">Close</span>
72
  </SheetPrimitive.Close>
 
73
  </SheetPrimitive.Content>
74
  </SheetPortal>
75
  ))
frontend/src/lib/types.ts CHANGED
@@ -3,6 +3,15 @@ export interface Message {
3
  content: string;
4
  role: "user" | "assistant" | "system";
5
  timestamp: Date;
 
 
 
 
 
 
 
 
 
6
  }
7
 
8
  export interface ChatState {
 
3
  content: string;
4
  role: "user" | "assistant" | "system";
5
  timestamp: Date;
6
+ media?: {
7
+ images?: string[];
8
+ videos?: string[];
9
+ links?: Array<{
10
+ text: string;
11
+ url: string;
12
+ }>;
13
+ references?: any[];
14
+ };
15
  }
16
 
17
  export interface ChatState {