zrguo commited on
Commit
a59011e
·
unverified ·
2 Parent(s): 85fc9f9 fc40a87

Merge pull request #792 from ArnoChenFx/light-webui

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lightrag/api/lightrag_server.py +63 -19
  2. lightrag/api/static/README.md +0 -2
  3. lightrag/api/static/favicon.ico +0 -3
  4. lightrag/api/static/index.html +0 -104
  5. lightrag/api/static/js/api.js +0 -408
  6. lightrag/api/static/js/graph.js +0 -211
  7. lightrag/api/webui/assets/index-BMB0OroL.js +0 -0
  8. lightrag/api/webui/assets/index-CF-pcoIl.js +0 -0
  9. lightrag/api/webui/assets/{index-BAeLPZpd.css → index-CLgSwrjG.css} +0 -0
  10. lightrag/api/webui/index.html +0 -0
  11. lightrag/lightrag.py +10 -0
  12. lightrag_webui/README.md +1 -1
  13. lightrag_webui/bun.lock +77 -10
  14. lightrag_webui/eslint.config.js +26 -23
  15. lightrag_webui/index.html +1 -1
  16. lightrag_webui/package.json +16 -5
  17. lightrag_webui/src/App.tsx +59 -9
  18. lightrag_webui/src/api/lightrag.ts +196 -158
  19. lightrag_webui/src/components/ApiKeyAlert.tsx +77 -0
  20. lightrag_webui/src/components/MessageAlert.tsx +5 -4
  21. lightrag_webui/src/components/documents/ClearDocumentsDialog.tsx +52 -0
  22. lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx +91 -0
  23. lightrag_webui/src/components/{FocusOnNode.tsx → graph/FocusOnNode.tsx} +0 -0
  24. lightrag_webui/src/components/{FullScreenControl.tsx → graph/FullScreenControl.tsx} +0 -0
  25. lightrag_webui/src/components/{GraphControl.tsx → graph/GraphControl.tsx} +0 -0
  26. lightrag_webui/src/components/{GraphLabels.tsx → graph/GraphLabels.tsx} +0 -0
  27. lightrag_webui/src/components/{GraphSearch.tsx → graph/GraphSearch.tsx} +0 -0
  28. lightrag_webui/src/components/{LayoutsControl.tsx → graph/LayoutsControl.tsx} +0 -0
  29. lightrag_webui/src/components/{PropertiesView.tsx → graph/PropertiesView.tsx} +0 -0
  30. lightrag_webui/src/components/{Settings.tsx → graph/Settings.tsx} +2 -2
  31. lightrag_webui/src/components/{StatusCard.tsx → graph/StatusCard.tsx} +0 -2
  32. lightrag_webui/src/components/{StatusIndicator.tsx → graph/StatusIndicator.tsx} +1 -1
  33. lightrag_webui/src/components/{ThemeProvider.tsx → graph/ThemeProvider.tsx} +0 -0
  34. lightrag_webui/src/components/{ThemeToggle.tsx → graph/ThemeToggle.tsx} +2 -0
  35. lightrag_webui/src/components/{ZoomControl.tsx → graph/ZoomControl.tsx} +0 -0
  36. lightrag_webui/src/components/retrieval/QuerySettings.tsx +279 -0
  37. lightrag_webui/src/components/ui/AlertDialog.tsx +115 -0
  38. lightrag_webui/src/components/ui/AsyncSearch.tsx +1 -1
  39. lightrag_webui/src/components/ui/Badge.tsx +33 -0
  40. lightrag_webui/src/components/ui/Button.tsx +2 -1
  41. lightrag_webui/src/components/ui/Card.tsx +55 -0
  42. lightrag_webui/src/components/ui/Checkbox.tsx +1 -1
  43. lightrag_webui/src/components/ui/DataTable.tsx +64 -0
  44. lightrag_webui/src/components/ui/Dialog.tsx +3 -3
  45. lightrag_webui/src/components/ui/EmptyCard.tsx +38 -0
  46. lightrag_webui/src/components/ui/FileUploader.tsx +322 -0
  47. lightrag_webui/src/components/ui/NumberInput.tsx +131 -0
  48. lightrag_webui/src/components/ui/Progress.tsx +23 -0
  49. lightrag_webui/src/components/ui/ScrollArea.tsx +44 -0
  50. lightrag_webui/src/components/ui/Select.tsx +151 -0
lightrag/api/lightrag_server.py CHANGED
@@ -19,6 +19,7 @@ from lightrag import LightRAG, QueryParam
19
  from lightrag.types import GPTKeywordExtractionFormat
20
  from lightrag.api import __api_version__
21
  from lightrag.utils import EmbeddingFunc
 
22
  from enum import Enum
23
  from pathlib import Path
24
  import shutil
@@ -253,10 +254,8 @@ def display_splash_screen(args: argparse.Namespace) -> None:
253
  ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/docs")
254
  ASCIIColors.white(" ├─ Alternative Documentation (local): ", end="")
255
  ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/redoc")
256
- ASCIIColors.white(" ├─ WebUI (local): ", end="")
257
  ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/webui")
258
- ASCIIColors.white(" └─ Graph Viewer (local): ", end="")
259
- ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/graph-viewer")
260
 
261
  ASCIIColors.yellow("\n📝 Note:")
262
  ASCIIColors.white(""" Since the server is running on 0.0.0.0:
@@ -693,6 +692,22 @@ class InsertResponse(BaseModel):
693
  message: str
694
 
695
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  def QueryRequestToQueryParams(request: QueryRequest):
697
  param = QueryParam(mode=request.mode, stream=request.stream)
698
  if request.only_need_context is not None:
@@ -1728,20 +1743,57 @@ def create_app(args):
1728
  app.include_router(ollama_api.router, prefix="/api")
1729
 
1730
  @app.get("/documents", dependencies=[Depends(optional_api_key)])
1731
- async def documents():
1732
- """Get current system status"""
1733
- return doc_manager.indexed_files
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1734
 
1735
  @app.get("/health", dependencies=[Depends(optional_api_key)])
1736
  async def get_status():
1737
  """Get current system status"""
1738
- files = doc_manager.scan_directory()
1739
  return {
1740
  "status": "healthy",
1741
  "working_directory": str(args.working_dir),
1742
  "input_directory": str(args.input_dir),
1743
- "indexed_files": [str(f) for f in files],
1744
- "indexed_files_count": len(files),
1745
  "configuration": {
1746
  # LLM configuration binding/host address (if applicable)/model (if applicable)
1747
  "llm_binding": args.llm_binding,
@@ -1760,17 +1812,9 @@ def create_app(args):
1760
  }
1761
 
1762
  # Webui mount webui/index.html
1763
- webui_dir = Path(__file__).parent / "webui"
1764
- app.mount(
1765
- "/graph-viewer",
1766
- StaticFiles(directory=webui_dir, html=True),
1767
- name="webui",
1768
- )
1769
-
1770
- # Serve the static files
1771
- static_dir = Path(__file__).parent / "static"
1772
  static_dir.mkdir(exist_ok=True)
1773
- app.mount("/webui", StaticFiles(directory=static_dir, html=True), name="static")
1774
 
1775
  return app
1776
 
 
19
  from lightrag.types import GPTKeywordExtractionFormat
20
  from lightrag.api import __api_version__
21
  from lightrag.utils import EmbeddingFunc
22
+ from lightrag.base import DocStatus, DocProcessingStatus
23
  from enum import Enum
24
  from pathlib import Path
25
  import shutil
 
254
  ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/docs")
255
  ASCIIColors.white(" ├─ Alternative Documentation (local): ", end="")
256
  ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/redoc")
257
+ ASCIIColors.white(" └─ WebUI (local): ", end="")
258
  ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/webui")
 
 
259
 
260
  ASCIIColors.yellow("\n📝 Note:")
261
  ASCIIColors.white(""" Since the server is running on 0.0.0.0:
 
692
  message: str
693
 
694
 
695
+ class DocStatusResponse(BaseModel):
696
+ id: str
697
+ content_summary: str
698
+ content_length: int
699
+ status: DocStatus
700
+ created_at: str
701
+ updated_at: str
702
+ chunks_count: Optional[int] = None
703
+ error: Optional[str] = None
704
+ metadata: Optional[dict[str, Any]] = None
705
+
706
+
707
+ class DocsStatusesResponse(BaseModel):
708
+ statuses: Dict[DocStatus, List[DocStatusResponse]] = {}
709
+
710
+
711
  def QueryRequestToQueryParams(request: QueryRequest):
712
  param = QueryParam(mode=request.mode, stream=request.stream)
713
  if request.only_need_context is not None:
 
1743
  app.include_router(ollama_api.router, prefix="/api")
1744
 
1745
  @app.get("/documents", dependencies=[Depends(optional_api_key)])
1746
+ async def documents() -> DocsStatusesResponse:
1747
+ """
1748
+ Get documents statuses
1749
+ Returns:
1750
+ DocsStatusesResponse: A response object containing a dictionary where keys are DocStatus
1751
+ and values are lists of DocStatusResponse objects representing documents in each status category.
1752
+ """
1753
+ try:
1754
+ statuses = (
1755
+ DocStatus.PENDING,
1756
+ DocStatus.PROCESSING,
1757
+ DocStatus.PROCESSED,
1758
+ DocStatus.FAILED,
1759
+ )
1760
+
1761
+ tasks = [rag.get_docs_by_status(status) for status in statuses]
1762
+ results: List[Dict[str, DocProcessingStatus]] = await asyncio.gather(*tasks)
1763
+
1764
+ response = DocsStatusesResponse()
1765
+
1766
+ for idx, result in enumerate(results):
1767
+ status = statuses[idx]
1768
+ for doc_id, doc_status in result.items():
1769
+ if status not in response.statuses:
1770
+ response.statuses[status] = []
1771
+ response.statuses[status].append(
1772
+ DocStatusResponse(
1773
+ id=doc_id,
1774
+ content_summary=doc_status.content_summary,
1775
+ content_length=doc_status.content_length,
1776
+ status=doc_status.status,
1777
+ created_at=doc_status.created_at,
1778
+ updated_at=doc_status.updated_at,
1779
+ chunks_count=doc_status.chunks_count,
1780
+ error=doc_status.error,
1781
+ metadata=doc_status.metadata,
1782
+ )
1783
+ )
1784
+ return response
1785
+ except Exception as e:
1786
+ logging.error(f"Error GET /documents: {str(e)}")
1787
+ logging.error(traceback.format_exc())
1788
+ raise HTTPException(status_code=500, detail=str(e))
1789
 
1790
  @app.get("/health", dependencies=[Depends(optional_api_key)])
1791
  async def get_status():
1792
  """Get current system status"""
 
1793
  return {
1794
  "status": "healthy",
1795
  "working_directory": str(args.working_dir),
1796
  "input_directory": str(args.input_dir),
 
 
1797
  "configuration": {
1798
  # LLM configuration binding/host address (if applicable)/model (if applicable)
1799
  "llm_binding": args.llm_binding,
 
1812
  }
1813
 
1814
  # Webui mount webui/index.html
1815
+ static_dir = Path(__file__).parent / "webui"
 
 
 
 
 
 
 
 
1816
  static_dir.mkdir(exist_ok=True)
1817
+ app.mount("/webui", StaticFiles(directory=static_dir, html=True), name="webui")
1818
 
1819
  return app
1820
 
lightrag/api/static/README.md DELETED
@@ -1,2 +0,0 @@
1
- # LightRag Webui
2
- A simple webui to interact with the lightrag datalake
 
 
 
lightrag/api/static/favicon.ico DELETED

Git LFS Details

  • SHA256: 26d6dfa1f5357416c10b39969c6e22843f58c518928bc59e828660ba5746ef94
  • Pointer size: 131 Bytes
  • Size of remote file: 751 kB
lightrag/api/static/index.html DELETED
@@ -1,104 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>LightRAG Interface</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
- <style>
10
- .fade-in {
11
- animation: fadeIn 0.3s ease-in;
12
- }
13
-
14
- @keyframes fadeIn {
15
- from { opacity: 0; }
16
- to { opacity: 1; }
17
- }
18
-
19
- .spin {
20
- animation: spin 1s linear infinite;
21
- }
22
-
23
- @keyframes spin {
24
- from { transform: rotate(0deg); }
25
- to { transform: rotate(360deg); }
26
- }
27
-
28
- .slide-in {
29
- animation: slideIn 0.3s ease-out;
30
- }
31
-
32
- @keyframes slideIn {
33
- from { transform: translateX(-100%); }
34
- to { transform: translateX(0); }
35
- }
36
- </style>
37
- </head>
38
- <body class="bg-gray-50">
39
- <div class="flex h-screen">
40
- <!-- Sidebar -->
41
- <div class="w-64 bg-white shadow-lg">
42
- <div class="p-4">
43
- <h1 class="text-xl font-bold text-gray-800 mb-6">LightRAG</h1>
44
- <nav class="space-y-2">
45
- <a href="#" class="nav-item" data-page="file-manager">
46
- <div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
47
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
49
- </svg>
50
- File Manager
51
- </div>
52
- </a>
53
- <a href="#" class="nav-item" data-page="query">
54
- <div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
55
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
57
- </svg>
58
- Query Database
59
- </div>
60
- </a>
61
- <a href="#" class="nav-item" data-page="knowledge-graph">
62
- <div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
63
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
64
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
65
- </svg>
66
- Knowledge Graph
67
- </div>
68
- </a>
69
- <a href="#" class="nav-item" data-page="status">
70
- <div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
71
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
72
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
73
- </svg>
74
- Status
75
- </div>
76
- </a>
77
- <a href="#" class="nav-item" data-page="settings">
78
- <div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
79
- <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
80
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
81
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
82
- </svg>
83
- Settings
84
- </div>
85
- </a>
86
- </nav>
87
- </div>
88
- </div>
89
-
90
- <!-- Main Content -->
91
- <div class="flex-1 overflow-auto p-6">
92
- <div id="content" class="fade-in"></div>
93
- </div>
94
-
95
- <!-- Toast Notification -->
96
- <div id="toast" class="fixed bottom-4 right-4 hidden">
97
- <div class="bg-gray-800 text-white px-6 py-3 rounded-lg shadow-lg"></div>
98
- </div>
99
- </div>
100
-
101
- <script src="./js/api.js"></script>
102
-
103
- </body>
104
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lightrag/api/static/js/api.js DELETED
@@ -1,408 +0,0 @@
1
- // State management
2
- const state = {
3
- apiKey: localStorage.getItem('apiKey') || '',
4
- files: [],
5
- indexedFiles: [],
6
- currentPage: 'file-manager'
7
- };
8
-
9
- // Utility functions
10
- const showToast = (message, duration = 3000) => {
11
- const toast = document.getElementById('toast');
12
- toast.querySelector('div').textContent = message;
13
- toast.classList.remove('hidden');
14
- setTimeout(() => toast.classList.add('hidden'), duration);
15
- };
16
-
17
- const fetchWithAuth = async (url, options = {}) => {
18
- const headers = {
19
- ...(options.headers || {}),
20
- ...(state.apiKey ? { 'X-API-Key': state.apiKey } : {}) // Use X-API-Key instead of Bearer
21
- };
22
- return fetch(url, { ...options, headers });
23
- };
24
-
25
-
26
- // Page renderers
27
- const pages = {
28
- 'file-manager': () => `
29
- <div class="space-y-6">
30
- <h2 class="text-2xl font-bold text-gray-800">File Manager</h2>
31
-
32
- <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
33
- <input type="file" id="fileInput" multiple accept=".txt,.md,.doc,.docx,.pdf,.pptx" class="hidden">
34
- <label for="fileInput" class="cursor-pointer">
35
- <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
37
- </svg>
38
- <p class="mt-2 text-gray-600">Drag files here or click to select</p>
39
- <p class="text-sm text-gray-500">Supported formats: TXT, MD, DOC, PDF, PPTX</p>
40
- </label>
41
- </div>
42
-
43
- <div id="fileList" class="space-y-2">
44
- <h3 class="text-lg font-semibold text-gray-700">Selected Files</h3>
45
- <div class="space-y-2"></div>
46
- </div>
47
- <div id="uploadProgress" class="hidden mt-4">
48
- <div class="w-full bg-gray-200 rounded-full h-2.5">
49
- <div class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
50
- </div>
51
- <p class="text-sm text-gray-600 mt-2"><span id="uploadStatus">0</span> files processed</p>
52
- </div>
53
- <div class="flex items-center space-x-4 bg-gray-100 p-4 rounded-lg shadow-md">
54
- <button id="rescanBtn" class="flex items-center bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
55
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" class="mr-2">
56
- <path d="M12 4a8 8 0 1 1-8 8H2.5a9.5 9.5 0 1 0 2.8-6.7L2 3v6h6L5.7 6.7A7.96 7.96 0 0 1 12 4z"/>
57
- </svg>
58
- Rescan Files
59
- </button>
60
-
61
- <button id="uploadBtn" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
62
- Upload & Index Files
63
- </button>
64
- </div>
65
-
66
- <div id="indexedFiles" class="space-y-2">
67
- <h3 class="text-lg font-semibold text-gray-700">Indexed Files</h3>
68
- <div class="space-y-2"></div>
69
- </div>
70
-
71
-
72
- </div>
73
- `,
74
-
75
- 'query': () => `
76
- <div class="space-y-6">
77
- <h2 class="text-2xl font-bold text-gray-800">Query Database</h2>
78
-
79
- <div class="space-y-4">
80
- <div>
81
- <label class="block text-sm font-medium text-gray-700">Query Mode</label>
82
- <select id="queryMode" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
83
- <option value="hybrid">Hybrid</option>
84
- <option value="local">Local</option>
85
- <option value="global">Global</option>
86
- <option value="naive">Naive</option>
87
- </select>
88
- </div>
89
-
90
- <div>
91
- <label class="block text-sm font-medium text-gray-700">Query</label>
92
- <textarea id="queryInput" rows="4" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
93
- </div>
94
-
95
- <button id="queryBtn" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
96
- Send Query
97
- </button>
98
-
99
- <div id="queryResult" class="mt-4 p-4 bg-white rounded-lg shadow"></div>
100
- </div>
101
- </div>
102
- `,
103
-
104
- 'knowledge-graph': () => `
105
- <div class="flex items-center justify-center h-full">
106
- <div class="text-center">
107
- <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
108
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
109
- </svg>
110
- <h3 class="mt-2 text-sm font-medium text-gray-900">Under Construction</h3>
111
- <p class="mt-1 text-sm text-gray-500">Knowledge graph visualization will be available in a future update.</p>
112
- </div>
113
- </div>
114
- `,
115
-
116
- 'status': () => `
117
- <div class="space-y-6">
118
- <h2 class="text-2xl font-bold text-gray-800">System Status</h2>
119
- <div id="statusContent" class="grid grid-cols-1 md:grid-cols-2 gap-6">
120
- <div class="p-6 bg-white rounded-lg shadow-sm">
121
- <h3 class="text-lg font-semibold mb-4">System Health</h3>
122
- <div id="healthStatus"></div>
123
- </div>
124
- <div class="p-6 bg-white rounded-lg shadow-sm">
125
- <h3 class="text-lg font-semibold mb-4">Configuration</h3>
126
- <div id="configStatus"></div>
127
- </div>
128
- </div>
129
- </div>
130
- `,
131
-
132
- 'settings': () => `
133
- <div class="space-y-6">
134
- <h2 class="text-2xl font-bold text-gray-800">Settings</h2>
135
-
136
- <div class="max-w-xl">
137
- <div class="space-y-4">
138
- <div>
139
- <label class="block text-sm font-medium text-gray-700">API Key</label>
140
- <input type="password" id="apiKeyInput" value="${state.apiKey}"
141
- class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
142
- </div>
143
-
144
- <button id="saveSettings" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
145
- Save Settings
146
- </button>
147
- </div>
148
- </div>
149
- </div>
150
- `
151
- };
152
-
153
- // Page handlers
154
- const handlers = {
155
- 'file-manager': () => {
156
- const fileInput = document.getElementById('fileInput');
157
- const dropZone = fileInput.parentElement.parentElement;
158
- const fileList = document.querySelector('#fileList div');
159
- const indexedFiles = document.querySelector('#indexedFiles div');
160
- const uploadBtn = document.getElementById('uploadBtn');
161
-
162
- const updateFileList = () => {
163
- fileList.innerHTML = state.files.map(file => `
164
- <div class="flex items-center justify-between bg-white p-3 rounded-lg shadow-sm">
165
- <span>${file.name}</span>
166
- <button class="text-red-600 hover:text-red-700" onclick="removeFile('${file.name}')">
167
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
168
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
169
- </svg>
170
- </button>
171
- </div>
172
- `).join('');
173
- };
174
-
175
- const updateIndexedFiles = async () => {
176
- const response = await fetchWithAuth('/health');
177
- const data = await response.json();
178
- indexedFiles.innerHTML = data.indexed_files.map(file => `
179
- <div class="flex items-center justify-between bg-white p-3 rounded-lg shadow-sm">
180
- <span>${file}</span>
181
- </div>
182
- `).join('');
183
- };
184
-
185
- dropZone.addEventListener('dragover', (e) => {
186
- e.preventDefault();
187
- dropZone.classList.add('border-blue-500');
188
- });
189
-
190
- dropZone.addEventListener('dragleave', () => {
191
- dropZone.classList.remove('border-blue-500');
192
- });
193
-
194
- dropZone.addEventListener('drop', (e) => {
195
- e.preventDefault();
196
- dropZone.classList.remove('border-blue-500');
197
- const files = Array.from(e.dataTransfer.files);
198
- state.files.push(...files);
199
- updateFileList();
200
- });
201
-
202
- fileInput.addEventListener('change', () => {
203
- state.files.push(...Array.from(fileInput.files));
204
- updateFileList();
205
- });
206
-
207
- uploadBtn.addEventListener('click', async () => {
208
- if (state.files.length === 0) {
209
- showToast('Please select files to upload');
210
- return;
211
- }
212
- let apiKey = localStorage.getItem('apiKey') || '';
213
- const progress = document.getElementById('uploadProgress');
214
- const progressBar = progress.querySelector('div');
215
- const statusText = document.getElementById('uploadStatus');
216
- progress.classList.remove('hidden');
217
-
218
- for (let i = 0; i < state.files.length; i++) {
219
- const formData = new FormData();
220
- formData.append('file', state.files[i]);
221
-
222
- try {
223
- await fetch('/documents/upload', {
224
- method: 'POST',
225
- headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {},
226
- body: formData
227
- });
228
-
229
- const percentage = ((i + 1) / state.files.length) * 100;
230
- progressBar.style.width = `${percentage}%`;
231
- statusText.textContent = `${i + 1}/${state.files.length}`;
232
- } catch (error) {
233
- console.error('Upload error:', error);
234
- }
235
- }
236
- progress.classList.add('hidden');
237
- });
238
-
239
- rescanBtn.addEventListener('click', async () => {
240
- const progress = document.getElementById('uploadProgress');
241
- const progressBar = progress.querySelector('div');
242
- const statusText = document.getElementById('uploadStatus');
243
- progress.classList.remove('hidden');
244
-
245
- try {
246
- // Start the scanning process
247
- const scanResponse = await fetch('/documents/scan', {
248
- method: 'POST',
249
- });
250
-
251
- if (!scanResponse.ok) {
252
- throw new Error('Scan failed to start');
253
- }
254
-
255
- // Start polling for progress
256
- const pollInterval = setInterval(async () => {
257
- const progressResponse = await fetch('/documents/scan-progress');
258
- const progressData = await progressResponse.json();
259
-
260
- // Update progress bar
261
- progressBar.style.width = `${progressData.progress}%`;
262
-
263
- // Update status text
264
- if (progressData.total_files > 0) {
265
- statusText.textContent = `Processing ${progressData.current_file} (${progressData.indexed_count}/${progressData.total_files})`;
266
- }
267
-
268
- // Check if scanning is complete
269
- if (!progressData.is_scanning) {
270
- clearInterval(pollInterval);
271
- progress.classList.add('hidden');
272
- statusText.textContent = 'Scan complete!';
273
- }
274
- }, 1000); // Poll every second
275
-
276
- } catch (error) {
277
- console.error('Upload error:', error);
278
- progress.classList.add('hidden');
279
- statusText.textContent = 'Error during scanning process';
280
- }
281
- });
282
-
283
-
284
- updateIndexedFiles();
285
- },
286
-
287
- 'query': () => {
288
- const queryBtn = document.getElementById('queryBtn');
289
- const queryInput = document.getElementById('queryInput');
290
- const queryMode = document.getElementById('queryMode');
291
- const queryResult = document.getElementById('queryResult');
292
-
293
- let apiKey = localStorage.getItem('apiKey') || '';
294
-
295
- queryBtn.addEventListener('click', async () => {
296
- const query = queryInput.value.trim();
297
- if (!query) {
298
- showToast('Please enter a query');
299
- return;
300
- }
301
-
302
- queryBtn.disabled = true;
303
- queryBtn.innerHTML = `
304
- <svg class="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
305
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
306
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
307
- </svg>
308
- Processing...
309
- `;
310
-
311
- try {
312
- const response = await fetchWithAuth('/query', {
313
- method: 'POST',
314
- headers: { 'Content-Type': 'application/json' },
315
- body: JSON.stringify({
316
- query,
317
- mode: queryMode.value,
318
- stream: false,
319
- only_need_context: false
320
- })
321
- });
322
-
323
- const data = await response.json();
324
- queryResult.innerHTML = marked.parse(data.response);
325
- } catch (error) {
326
- showToast('Error processing query');
327
- } finally {
328
- queryBtn.disabled = false;
329
- queryBtn.textContent = 'Send Query';
330
- }
331
- });
332
- },
333
-
334
- 'status': async () => {
335
- const healthStatus = document.getElementById('healthStatus');
336
- const configStatus = document.getElementById('configStatus');
337
-
338
- try {
339
- const response = await fetchWithAuth('/health');
340
- const data = await response.json();
341
-
342
- healthStatus.innerHTML = `
343
- <div class="space-y-2">
344
- <div class="flex items-center">
345
- <div class="w-3 h-3 rounded-full ${data.status === 'healthy' ? 'bg-green-500' : 'bg-red-500'} mr-2"></div>
346
- <span class="font-medium">${data.status}</span>
347
- </div>
348
- <div>
349
- <p class="text-sm text-gray-600">Working Directory: ${data.working_directory}</p>
350
- <p class="text-sm text-gray-600">Input Directory: ${data.input_directory}</p>
351
- <p class="text-sm text-gray-600">Indexed Files: ${data.indexed_files_count}</p>
352
- </div>
353
- </div>
354
- `;
355
-
356
- configStatus.innerHTML = Object.entries(data.configuration)
357
- .map(([key, value]) => `
358
- <div class="mb-2">
359
- <span class="text-sm font-medium text-gray-700">${key}:</span>
360
- <span class="text-sm text-gray-600 ml-2">${value}</span>
361
- </div>
362
- `).join('');
363
- } catch (error) {
364
- showToast('Error fetching status');
365
- }
366
- },
367
-
368
- 'settings': () => {
369
- const saveBtn = document.getElementById('saveSettings');
370
- const apiKeyInput = document.getElementById('apiKeyInput');
371
-
372
- saveBtn.addEventListener('click', () => {
373
- state.apiKey = apiKeyInput.value;
374
- localStorage.setItem('apiKey', state.apiKey);
375
- showToast('Settings saved successfully');
376
- });
377
- }
378
- };
379
-
380
- // Navigation handling
381
- document.querySelectorAll('.nav-item').forEach(item => {
382
- item.addEventListener('click', (e) => {
383
- e.preventDefault();
384
- const page = item.dataset.page;
385
- document.getElementById('content').innerHTML = pages[page]();
386
- if (handlers[page]) handlers[page]();
387
- state.currentPage = page;
388
- });
389
- });
390
-
391
- // Initialize with file manager
392
- document.getElementById('content').innerHTML = pages['file-manager']();
393
- handlers['file-manager']();
394
-
395
- // Global functions
396
- window.removeFile = (fileName) => {
397
- state.files = state.files.filter(file => file.name !== fileName);
398
- document.querySelector('#fileList div').innerHTML = state.files.map(file => `
399
- <div class="flex items-center justify-between bg-white p-3 rounded-lg shadow-sm">
400
- <span>${file.name}</span>
401
- <button class="text-red-600 hover:text-red-700" onclick="removeFile('${file.name}')">
402
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
403
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
404
- </svg>
405
- </button>
406
- </div>
407
- `).join('');
408
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lightrag/api/static/js/graph.js DELETED
@@ -1,211 +0,0 @@
1
- // js/graph.js
2
- function openGraphModal(label) {
3
- const modal = document.getElementById("graph-modal");
4
- const graphTitle = document.getElementById("graph-title");
5
-
6
- if (!modal || !graphTitle) {
7
- console.error("Key element not found");
8
- return;
9
- }
10
-
11
- graphTitle.textContent = `Knowledge Graph - ${label}`;
12
- modal.style.display = "flex";
13
-
14
- renderGraph(label);
15
- }
16
-
17
- function closeGraphModal() {
18
- const modal = document.getElementById("graph-modal");
19
- modal.style.display = "none";
20
- clearGraph();
21
- }
22
-
23
- function clearGraph() {
24
- const svg = document.getElementById("graph-svg");
25
- svg.innerHTML = "";
26
- }
27
-
28
-
29
- async function getGraph(label) {
30
- try {
31
- const response = await fetch(`/graphs?label=${label}`);
32
- const rawData = await response.json();
33
- console.log({data: JSON.parse(JSON.stringify(rawData))});
34
-
35
- const nodes = rawData.nodes
36
-
37
- nodes.forEach(node => {
38
- node.id = Date.now().toString(36) + Math.random().toString(36).substring(2); // 使用 crypto.randomUUID() 生成唯一 UUID
39
- });
40
-
41
- // Strictly verify edge data
42
- const edges = (rawData.edges || []).map(edge => {
43
- const sourceNode = nodes.find(n => n.labels.includes(edge.source));
44
- const targetNode = nodes.find(n => n.labels.includes(edge.target)
45
- )
46
- ;
47
- if (!sourceNode || !targetNode) {
48
- console.warn("NOT VALID EDGE:", edge);
49
- return null;
50
- }
51
- return {
52
- source: sourceNode,
53
- target: targetNode,
54
- type: edge.type || ""
55
- };
56
- }).filter(edge => edge !== null);
57
-
58
- return {nodes, edges};
59
- } catch (error) {
60
- console.error("Loading graph failed:", error);
61
- return {nodes: [], edges: []};
62
- }
63
- }
64
-
65
- async function renderGraph(label) {
66
- const data = await getGraph(label);
67
-
68
-
69
- if (!data.nodes || data.nodes.length === 0) {
70
- d3.select("#graph-svg")
71
- .html(`<text x="50%" y="50%" text-anchor="middle">No valid nodes</text>`);
72
- return;
73
- }
74
-
75
-
76
- const svg = d3.select("#graph-svg");
77
- const width = svg.node().clientWidth;
78
- const height = svg.node().clientHeight;
79
-
80
- svg.selectAll("*").remove();
81
-
82
- // Create a force oriented diagram layout
83
- const simulation = d3.forceSimulation(data.nodes)
84
- .force("charge", d3.forceManyBody().strength(-300))
85
- .force("center", d3.forceCenter(width / 2, height / 2));
86
-
87
- // Add a connection (if there are valid edges)
88
- if (data.edges.length > 0) {
89
- simulation.force("link",
90
- d3.forceLink(data.edges)
91
- .id(d => d.id)
92
- .distance(100)
93
- );
94
- }
95
-
96
- // Draw nodes
97
- const nodes = svg.selectAll(".node")
98
- .data(data.nodes)
99
- .enter()
100
- .append("circle")
101
- .attr("class", "node")
102
- .attr("r", 10)
103
- .call(d3.drag()
104
- .on("start", dragStarted)
105
- .on("drag", dragged)
106
- .on("end", dragEnded)
107
- );
108
-
109
-
110
- svg.append("defs")
111
- .append("marker")
112
- .attr("id", "arrow-out")
113
- .attr("viewBox", "0 0 10 10")
114
- .attr("refX", 8)
115
- .attr("refY", 5)
116
- .attr("markerWidth", 6)
117
- .attr("markerHeight", 6)
118
- .attr("orient", "auto")
119
- .append("path")
120
- .attr("d", "M0,0 L10,5 L0,10 Z")
121
- .attr("fill", "#999");
122
-
123
- // Draw edges (with arrows)
124
- const links = svg.selectAll(".link")
125
- .data(data.edges)
126
- .enter()
127
- .append("line")
128
- .attr("class", "link")
129
- .attr("marker-end", "url(#arrow-out)"); // Always draw arrows on the target side
130
-
131
- // Edge style configuration
132
- links
133
- .attr("stroke", "#999")
134
- .attr("stroke-width", 2)
135
- .attr("stroke-opacity", 0.8);
136
-
137
- // Draw label (with background box)
138
- const labels = svg.selectAll(".label")
139
- .data(data.nodes)
140
- .enter()
141
- .append("text")
142
- .attr("class", "label")
143
- .text(d => d.labels[0] || "")
144
- .attr("text-anchor", "start")
145
- .attr("dy", "0.3em")
146
- .attr("fill", "#333");
147
-
148
- // Update Location
149
- simulation.on("tick", () => {
150
- links
151
- .attr("x1", d => {
152
- // Calculate the direction vector from the source node to the target node
153
- const dx = d.target.x - d.source.x;
154
- const dy = d.target.y - d.source.y;
155
- const distance = Math.sqrt(dx * dx + dy * dy);
156
- if (distance === 0) return d.source.x; // 避免除以零 Avoid dividing by zero
157
- // Adjust the starting point coordinates (source node edge) based on radius 10
158
- return d.source.x + (dx / distance) * 10;
159
- })
160
- .attr("y1", d => {
161
- const dx = d.target.x - d.source.x;
162
- const dy = d.target.y - d.source.y;
163
- const distance = Math.sqrt(dx * dx + dy * dy);
164
- if (distance === 0) return d.source.y;
165
- return d.source.y + (dy / distance) * 10;
166
- })
167
- .attr("x2", d => {
168
- // Adjust the endpoint coordinates (target node edge) based on a radius of 10
169
- const dx = d.target.x - d.source.x;
170
- const dy = d.target.y - d.source.y;
171
- const distance = Math.sqrt(dx * dx + dy * dy);
172
- if (distance === 0) return d.target.x;
173
- return d.target.x - (dx / distance) * 10;
174
- })
175
- .attr("y2", d => {
176
- const dx = d.target.x - d.source.x;
177
- const dy = d.target.y - d.source.y;
178
- const distance = Math.sqrt(dx * dx + dy * dy);
179
- if (distance === 0) return d.target.y;
180
- return d.target.y - (dy / distance) * 10;
181
- });
182
-
183
- // Update the position of nodes and labels (keep unchanged)
184
- nodes
185
- .attr("cx", d => d.x)
186
- .attr("cy", d => d.y);
187
-
188
- labels
189
- .attr("x", d => d.x + 12)
190
- .attr("y", d => d.y + 4);
191
- });
192
-
193
- // Drag and drop logic
194
- function dragStarted(event, d) {
195
- if (!event.active) simulation.alphaTarget(0.3).restart();
196
- d.fx = d.x;
197
- d.fy = d.y;
198
- }
199
-
200
- function dragged(event, d) {
201
- d.fx = event.x;
202
- d.fy = event.y;
203
- simulation.alpha(0.3).restart();
204
- }
205
-
206
- function dragEnded(event, d) {
207
- if (!event.active) simulation.alphaTarget(0);
208
- d.fx = null;
209
- d.fy = null;
210
- }
211
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lightrag/api/webui/assets/index-BMB0OroL.js ADDED
Binary file (832 kB). View file
 
lightrag/api/webui/assets/index-CF-pcoIl.js DELETED
Binary file (609 kB)
 
lightrag/api/webui/assets/{index-BAeLPZpd.css → index-CLgSwrjG.css} RENAMED
Binary files a/lightrag/api/webui/assets/index-BAeLPZpd.css and b/lightrag/api/webui/assets/index-CLgSwrjG.css differ
 
lightrag/api/webui/index.html CHANGED
Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ
 
lightrag/lightrag.py CHANGED
@@ -1254,6 +1254,16 @@ class LightRAG:
1254
  """
1255
  return await self.doc_status.get_status_counts()
1256
 
 
 
 
 
 
 
 
 
 
 
1257
  async def adelete_by_doc_id(self, doc_id: str) -> None:
1258
  """Delete a document and all its related data
1259
 
 
1254
  """
1255
  return await self.doc_status.get_status_counts()
1256
 
1257
+ async def get_docs_by_status(
1258
+ self, status: DocStatus
1259
+ ) -> dict[str, DocProcessingStatus]:
1260
+ """Get documents by status
1261
+
1262
+ Returns:
1263
+ Dict with document id is keys and document status is values
1264
+ """
1265
+ return await self.doc_status.get_docs_by_status(status)
1266
+
1267
  async def adelete_by_doc_id(self, doc_id: str) -> None:
1268
  """Delete a document and all its related data
1269
 
lightrag_webui/README.md CHANGED
@@ -21,7 +21,7 @@ LightRAG WebUI is a React-based web interface for interacting with the LightRAG
21
  Run the following command to build the project:
22
 
23
  ```bash
24
- bun run build
25
  ```
26
 
27
  This command will bundle the project and output the built files to the `lightrag/api/webui` directory.
 
21
  Run the following command to build the project:
22
 
23
  ```bash
24
+ bun run build --emptyOutDir
25
  ```
26
 
27
  This command will bundle the project and output the built files to the `lightrag/api/webui` directory.
lightrag_webui/bun.lock CHANGED
@@ -4,13 +4,19 @@
4
  "": {
5
  "name": "lightrag-webui",
6
  "dependencies": {
7
- "@faker-js/faker": "^9.4.0",
 
8
  "@radix-ui/react-checkbox": "^1.1.4",
9
  "@radix-ui/react-dialog": "^1.1.6",
10
  "@radix-ui/react-popover": "^1.1.6",
 
 
 
11
  "@radix-ui/react-separator": "^1.1.2",
12
  "@radix-ui/react-slot": "^1.1.2",
 
13
  "@radix-ui/react-tooltip": "^1.1.8",
 
14
  "@react-sigma/core": "^5.0.2",
15
  "@react-sigma/graph-search": "^5.0.3",
16
  "@react-sigma/layout-circlepack": "^5.0.2",
@@ -22,6 +28,7 @@
22
  "@react-sigma/minimap": "^5.0.2",
23
  "@sigma/edge-curve": "^3.1.0",
24
  "@sigma/node-border": "^3.0.0",
 
25
  "class-variance-authority": "^0.7.1",
26
  "clsx": "^2.1.1",
27
  "cmdk": "^1.0.4",
@@ -31,9 +38,13 @@
31
  "minisearch": "^7.1.1",
32
  "react": "^19.0.0",
33
  "react-dom": "^19.0.0",
 
 
34
  "seedrandom": "^3.0.5",
35
  "sigma": "^3.0.1",
 
36
  "tailwind-merge": "^3.0.1",
 
37
  "zustand": "^5.0.3",
38
  },
39
  "devDependencies": {
@@ -41,19 +52,19 @@
41
  "@stylistic/eslint-plugin-js": "^3.1.0",
42
  "@tailwindcss/vite": "^4.0.6",
43
  "@types/bun": "^1.2.2",
44
- "@types/node": "^22.13.1",
45
  "@types/react": "^19.0.8",
46
  "@types/react-dom": "^19.0.3",
47
  "@types/seedrandom": "^3.0.8",
48
  "@vitejs/plugin-react-swc": "^3.8.0",
49
- "eslint": "^9.20.0",
50
  "eslint-config-prettier": "^10.0.1",
51
  "eslint-plugin-react": "^7.37.4",
52
  "eslint-plugin-react-hooks": "^5.1.0",
53
  "eslint-plugin-react-refresh": "^0.4.19",
54
- "globals": "^15.14.0",
55
  "graphology-types": "^0.24.8",
56
- "prettier": "^3.5.0",
57
  "prettier-plugin-tailwindcss": "^0.6.11",
58
  "tailwindcss": "^4.0.6",
59
  "tailwindcss-animate": "^1.0.7",
@@ -172,7 +183,7 @@
172
 
173
  "@eslint/plugin-kit": ["@eslint/[email protected]", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
174
 
175
- "@faker-js/faker": ["@faker-js/faker@9.4.0", "", {}, "sha512-85+k0AxaZSTowL0gXp8zYWDIrWclTbRPg/pm/V0dSFZ6W6D4lhcG3uuZl4zLsEKfEvs69xDbLN2cHQudwp95JA=="],
176
 
177
  "@floating-ui/core": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
178
 
@@ -206,18 +217,26 @@
206
 
207
  "@nodelib/fs.walk": ["@nodelib/[email protected]", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
208
 
 
 
209
  "@radix-ui/primitive": ["@radix-ui/[email protected]", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
210
 
 
 
211
  "@radix-ui/react-arrow": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
212
 
213
  "@radix-ui/react-checkbox": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="],
214
 
 
 
215
  "@radix-ui/react-compose-refs": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
216
 
217
  "@radix-ui/react-context": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
218
 
219
  "@radix-ui/react-dialog": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="],
220
 
 
 
221
  "@radix-ui/react-dismissable-layer": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
222
 
223
  "@radix-ui/react-focus-guards": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
@@ -236,10 +255,20 @@
236
 
237
  "@radix-ui/react-primitive": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
238
 
 
 
 
 
 
 
 
 
239
  "@radix-ui/react-separator": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
240
 
241
  "@radix-ui/react-slot": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
242
 
 
 
243
  "@radix-ui/react-tooltip": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
244
 
245
  "@radix-ui/react-use-callback-ref": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
@@ -384,10 +413,12 @@
384
 
385
  "@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
386
 
387
- "@types/node": ["@types/[email protected].1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
388
 
389
  "@types/parse-json": ["@types/[email protected]", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
390
 
 
 
391
  "@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="],
392
 
393
  "@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="],
@@ -446,8 +477,14 @@
446
 
447
  "async-function": ["[email protected]", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
448
 
 
 
 
 
449
  "available-typed-arrays": ["[email protected]", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
450
 
 
 
451
  "babel-plugin-macros": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
452
 
453
  "balanced-match": ["[email protected]", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -478,6 +515,8 @@
478
 
479
  "color-name": ["[email protected]", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
480
 
 
 
481
  "concat-map": ["[email protected]", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
482
 
483
  "convert-source-map": ["[email protected]", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
@@ -502,6 +541,8 @@
502
 
503
  "define-properties": ["[email protected]", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
504
 
 
 
505
  "detect-libc": ["[email protected]", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
506
 
507
  "detect-node-es": ["[email protected]", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
@@ -536,7 +577,7 @@
536
 
537
  "escape-string-regexp": ["[email protected]", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
538
 
539
- "eslint": ["[email protected].0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA=="],
540
 
541
  "eslint-config-prettier": ["[email protected]", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="],
542
 
@@ -574,6 +615,8 @@
574
 
575
  "file-entry-cache": ["[email protected]", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
576
 
 
 
577
  "fill-range": ["[email protected]", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
578
 
579
  "find-root": ["[email protected]", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
@@ -584,8 +627,12 @@
584
 
585
  "flatted": ["[email protected]", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
586
 
 
 
587
  "for-each": ["[email protected]", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw=="],
588
 
 
 
589
  "fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
590
 
591
  "function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -604,7 +651,7 @@
604
 
605
  "glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
606
 
607
- "globals": ["globals@15.14.0", "", {}, "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="],
608
 
609
  "globalthis": ["[email protected]", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
610
 
@@ -778,6 +825,10 @@
778
 
779
  "micromatch": ["[email protected]", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
780
 
 
 
 
 
781
  "minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
782
 
783
  "minisearch": ["[email protected]", "", {}, "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw=="],
@@ -838,12 +889,16 @@
838
 
839
  "prelude-ls": ["[email protected]", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
840
 
841
- "prettier": ["[email protected].0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="],
842
 
843
  "prettier-plugin-tailwindcss": ["[email protected]", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
844
 
 
 
845
  "prop-types": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
846
 
 
 
847
  "punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
848
 
849
  "queue-microtask": ["[email protected]", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@@ -852,8 +907,12 @@
852
 
853
  "react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
854
 
 
 
855
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
856
 
 
 
857
  "react-remove-scroll": ["[email protected]", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
858
 
859
  "react-remove-scroll-bar": ["[email protected]", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@@ -912,6 +971,8 @@
912
 
913
  "sigma": ["[email protected]", "", { "dependencies": { "events": "^3.3.0", "graphology-utils": "^2.5.2" } }, "sha512-z67BX1FhIpD+wLs2WJ7QS2aR49TcSr3YaVZ2zU8cAc5jMiUYlSbeDp4EI6euBDUpm3/lzO4pfytP/gW4BhXWuA=="],
914
 
 
 
915
  "source-map": ["[email protected]", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
916
 
917
  "source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -936,6 +997,8 @@
936
 
937
  "tailwind-merge": ["[email protected]", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
938
 
 
 
939
  "tailwindcss": ["[email protected]", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="],
940
 
941
  "tailwindcss-animate": ["[email protected]", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
@@ -1006,12 +1069,16 @@
1006
 
1007
  "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/[email protected]", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
1008
 
 
 
1009
  "@typescript-eslint/typescript-estree/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1010
 
1011
  "@typescript-eslint/typescript-estree/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
1012
 
1013
  "babel-plugin-macros/resolve": ["[email protected]", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
1014
 
 
 
1015
  "fast-glob/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
1016
 
1017
  "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
 
4
  "": {
5
  "name": "lightrag-webui",
6
  "dependencies": {
7
+ "@faker-js/faker": "^9.5.0",
8
+ "@radix-ui/react-alert-dialog": "^1.1.6",
9
  "@radix-ui/react-checkbox": "^1.1.4",
10
  "@radix-ui/react-dialog": "^1.1.6",
11
  "@radix-ui/react-popover": "^1.1.6",
12
+ "@radix-ui/react-progress": "^1.1.2",
13
+ "@radix-ui/react-scroll-area": "^1.2.3",
14
+ "@radix-ui/react-select": "^2.1.6",
15
  "@radix-ui/react-separator": "^1.1.2",
16
  "@radix-ui/react-slot": "^1.1.2",
17
+ "@radix-ui/react-tabs": "^1.1.3",
18
  "@radix-ui/react-tooltip": "^1.1.8",
19
+ "@radix-ui/react-use-controllable-state": "^1.1.0",
20
  "@react-sigma/core": "^5.0.2",
21
  "@react-sigma/graph-search": "^5.0.3",
22
  "@react-sigma/layout-circlepack": "^5.0.2",
 
28
  "@react-sigma/minimap": "^5.0.2",
29
  "@sigma/edge-curve": "^3.1.0",
30
  "@sigma/node-border": "^3.0.0",
31
+ "axios": "^1.7.9",
32
  "class-variance-authority": "^0.7.1",
33
  "clsx": "^2.1.1",
34
  "cmdk": "^1.0.4",
 
38
  "minisearch": "^7.1.1",
39
  "react": "^19.0.0",
40
  "react-dom": "^19.0.0",
41
+ "react-dropzone": "^14.3.5",
42
+ "react-number-format": "^5.4.3",
43
  "seedrandom": "^3.0.5",
44
  "sigma": "^3.0.1",
45
+ "sonner": "^1.7.4",
46
  "tailwind-merge": "^3.0.1",
47
+ "tailwind-scrollbar": "^4.0.0",
48
  "zustand": "^5.0.3",
49
  },
50
  "devDependencies": {
 
52
  "@stylistic/eslint-plugin-js": "^3.1.0",
53
  "@tailwindcss/vite": "^4.0.6",
54
  "@types/bun": "^1.2.2",
55
+ "@types/node": "^22.13.4",
56
  "@types/react": "^19.0.8",
57
  "@types/react-dom": "^19.0.3",
58
  "@types/seedrandom": "^3.0.8",
59
  "@vitejs/plugin-react-swc": "^3.8.0",
60
+ "eslint": "^9.20.1",
61
  "eslint-config-prettier": "^10.0.1",
62
  "eslint-plugin-react": "^7.37.4",
63
  "eslint-plugin-react-hooks": "^5.1.0",
64
  "eslint-plugin-react-refresh": "^0.4.19",
65
+ "globals": "^15.15.0",
66
  "graphology-types": "^0.24.8",
67
+ "prettier": "^3.5.1",
68
  "prettier-plugin-tailwindcss": "^0.6.11",
69
  "tailwindcss": "^4.0.6",
70
  "tailwindcss-animate": "^1.0.7",
 
183
 
184
  "@eslint/plugin-kit": ["@eslint/[email protected]", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
185
 
186
+ "@faker-js/faker": ["@faker-js/faker@9.5.0", "", {}, "sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw=="],
187
 
188
  "@floating-ui/core": ["@floating-ui/[email protected]", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
189
 
 
217
 
218
  "@nodelib/fs.walk": ["@nodelib/[email protected]", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
219
 
220
+ "@radix-ui/number": ["@radix-ui/[email protected]", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="],
221
+
222
  "@radix-ui/primitive": ["@radix-ui/[email protected]", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
223
 
224
+ "@radix-ui/react-alert-dialog": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dialog": "1.1.6", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ=="],
225
+
226
  "@radix-ui/react-arrow": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
227
 
228
  "@radix-ui/react-checkbox": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="],
229
 
230
+ "@radix-ui/react-collection": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="],
231
+
232
  "@radix-ui/react-compose-refs": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
233
 
234
  "@radix-ui/react-context": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
235
 
236
  "@radix-ui/react-dialog": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="],
237
 
238
+ "@radix-ui/react-direction": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="],
239
+
240
  "@radix-ui/react-dismissable-layer": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
241
 
242
  "@radix-ui/react-focus-guards": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
 
255
 
256
  "@radix-ui/react-primitive": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
257
 
258
+ "@radix-ui/react-progress": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA=="],
259
+
260
+ "@radix-ui/react-roving-focus": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="],
261
+
262
+ "@radix-ui/react-scroll-area": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ=="],
263
+
264
+ "@radix-ui/react-select": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg=="],
265
+
266
  "@radix-ui/react-separator": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
267
 
268
  "@radix-ui/react-slot": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
269
 
270
+ "@radix-ui/react-tabs": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng=="],
271
+
272
  "@radix-ui/react-tooltip": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
273
 
274
  "@radix-ui/react-use-callback-ref": ["@radix-ui/[email protected]", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
 
413
 
414
  "@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
415
 
416
+ "@types/node": ["@types/[email protected].4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
417
 
418
  "@types/parse-json": ["@types/[email protected]", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
419
 
420
+ "@types/prismjs": ["@types/[email protected]", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
421
+
422
  "@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="],
423
 
424
  "@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="],
 
477
 
478
  "async-function": ["[email protected]", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
479
 
480
+ "asynckit": ["[email protected]", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
481
+
482
+ "attr-accept": ["[email protected]", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
483
+
484
  "available-typed-arrays": ["[email protected]", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
485
 
486
+ "axios": ["[email protected]", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
487
+
488
  "babel-plugin-macros": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
489
 
490
  "balanced-match": ["[email protected]", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
515
 
516
  "color-name": ["[email protected]", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
517
 
518
+ "combined-stream": ["[email protected]", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
519
+
520
  "concat-map": ["[email protected]", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
521
 
522
  "convert-source-map": ["[email protected]", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
 
541
 
542
  "define-properties": ["[email protected]", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
543
 
544
+ "delayed-stream": ["[email protected]", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
545
+
546
  "detect-libc": ["[email protected]", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
547
 
548
  "detect-node-es": ["[email protected]", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
 
577
 
578
  "escape-string-regexp": ["[email protected]", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
579
 
580
+ "eslint": ["[email protected].1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="],
581
 
582
  "eslint-config-prettier": ["[email protected]", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="],
583
 
 
615
 
616
  "file-entry-cache": ["[email protected]", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
617
 
618
+ "file-selector": ["[email protected]", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
619
+
620
  "fill-range": ["[email protected]", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
621
 
622
  "find-root": ["[email protected]", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
 
627
 
628
  "flatted": ["[email protected]", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
629
 
630
+ "follow-redirects": ["[email protected]", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
631
+
632
  "for-each": ["[email protected]", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw=="],
633
 
634
+ "form-data": ["[email protected]", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
635
+
636
  "fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
637
 
638
  "function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 
651
 
652
  "glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
653
 
654
+ "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
655
 
656
  "globalthis": ["[email protected]", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
657
 
 
825
 
826
  "micromatch": ["[email protected]", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
827
 
828
+ "mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
829
+
830
+ "mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
831
+
832
  "minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
833
 
834
  "minisearch": ["[email protected]", "", {}, "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw=="],
 
889
 
890
  "prelude-ls": ["[email protected]", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
891
 
892
+ "prettier": ["[email protected].1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw=="],
893
 
894
  "prettier-plugin-tailwindcss": ["[email protected]", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
895
 
896
+ "prism-react-renderer": ["[email protected]", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
897
+
898
  "prop-types": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
899
 
900
+ "proxy-from-env": ["[email protected]", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
901
+
902
  "punycode": ["[email protected]", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
903
 
904
  "queue-microtask": ["[email protected]", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
 
907
 
908
  "react-dom": ["[email protected]", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
909
 
910
+ "react-dropzone": ["[email protected]", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ=="],
911
+
912
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
913
 
914
+ "react-number-format": ["[email protected]", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VCY5hFg/soBighAoGcdE+GagkJq0230qN6jcS5sp8wQX1qy1fYN/RX7/BXkrs0oyzzwqR8/+eSUrqXbGeywdUQ=="],
915
+
916
  "react-remove-scroll": ["[email protected]", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
917
 
918
  "react-remove-scroll-bar": ["[email protected]", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
 
971
 
972
  "sigma": ["[email protected]", "", { "dependencies": { "events": "^3.3.0", "graphology-utils": "^2.5.2" } }, "sha512-z67BX1FhIpD+wLs2WJ7QS2aR49TcSr3YaVZ2zU8cAc5jMiUYlSbeDp4EI6euBDUpm3/lzO4pfytP/gW4BhXWuA=="],
973
 
974
+ "sonner": ["[email protected]", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
975
+
976
  "source-map": ["[email protected]", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
977
 
978
  "source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 
997
 
998
  "tailwind-merge": ["[email protected]", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
999
 
1000
+ "tailwind-scrollbar": ["[email protected]", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-elqx9m09VHY8gkrMiyimFO09JlS3AyLFXT0eaLaWPi7ImwHlbZj1ce/AxSis2LtR+ewBGEyUV7URNEMcjP1Z2w=="],
1001
+
1002
  "tailwindcss": ["[email protected]", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="],
1003
 
1004
  "tailwindcss-animate": ["[email protected]", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
 
1069
 
1070
  "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/[email protected]", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
1071
 
1072
+ "@types/ws/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
1073
+
1074
  "@typescript-eslint/typescript-estree/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1075
 
1076
  "@typescript-eslint/typescript-estree/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
1077
 
1078
  "babel-plugin-macros/resolve": ["[email protected]", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
1079
 
1080
+ "bun-types/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
1081
+
1082
  "fast-glob/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
1083
 
1084
  "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
lightrag_webui/eslint.config.js CHANGED
@@ -7,27 +7,30 @@ import tseslint from 'typescript-eslint'
7
  import prettier from 'eslint-config-prettier'
8
  import react from 'eslint-plugin-react'
9
 
10
- export default tseslint.config({ ignores: ['dist'] }, prettier, {
11
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
12
- files: ['**/*.{ts,tsx,js,jsx}'],
13
- languageOptions: {
14
- ecmaVersion: 2020,
15
- globals: globals.browser
16
- },
17
- settings: { react: { version: '19.0' } },
18
- plugins: {
19
- 'react-hooks': reactHooks,
20
- 'react-refresh': reactRefresh,
21
- '@stylistic/js': stylisticJs,
22
- react
23
- },
24
- rules: {
25
- ...reactHooks.configs.recommended.rules,
26
- 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
27
- ...react.configs.recommended.rules,
28
- ...react.configs['jsx-runtime'].rules,
29
- '@stylistic/js/indent': ['error', 2],
30
- '@stylistic/js/quotes': ['error', 'single'],
31
- '@typescript-eslint/no-explicit-any': ['off']
 
 
 
32
  }
33
- })
 
7
  import prettier from 'eslint-config-prettier'
8
  import react from 'eslint-plugin-react'
9
 
10
+ export default tseslint.config(
11
+ { ignores: ['dist'] },
12
+ {
13
+ extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
14
+ files: ['**/*.{ts,tsx,js,jsx}'],
15
+ languageOptions: {
16
+ ecmaVersion: 2020,
17
+ globals: globals.browser
18
+ },
19
+ settings: { react: { version: '19.0' } },
20
+ plugins: {
21
+ 'react-hooks': reactHooks,
22
+ 'react-refresh': reactRefresh,
23
+ '@stylistic/js': stylisticJs,
24
+ react
25
+ },
26
+ rules: {
27
+ ...reactHooks.configs.recommended.rules,
28
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
29
+ ...react.configs.recommended.rules,
30
+ ...react.configs['jsx-runtime'].rules,
31
+ '@stylistic/js/indent': ['error', 2],
32
+ '@stylistic/js/quotes': ['error', 'single'],
33
+ '@typescript-eslint/no-explicit-any': ['off']
34
+ }
35
  }
36
+ )
lightrag_webui/index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Lightrag Graph Viewer</title>
8
  </head>
9
  <body>
10
  <div id="root"></div>
 
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Lightrag</title>
8
  </head>
9
  <body>
10
  <div id="root"></div>
lightrag_webui/package.json CHANGED
@@ -10,13 +10,19 @@
10
  "preview": "bunx --bun vite preview"
11
  },
12
  "dependencies": {
13
- "@faker-js/faker": "^9.4.0",
 
14
  "@radix-ui/react-checkbox": "^1.1.4",
15
  "@radix-ui/react-dialog": "^1.1.6",
16
  "@radix-ui/react-popover": "^1.1.6",
 
 
 
17
  "@radix-ui/react-separator": "^1.1.2",
18
  "@radix-ui/react-slot": "^1.1.2",
 
19
  "@radix-ui/react-tooltip": "^1.1.8",
 
20
  "@react-sigma/core": "^5.0.2",
21
  "@react-sigma/graph-search": "^5.0.3",
22
  "@react-sigma/layout-circlepack": "^5.0.2",
@@ -28,6 +34,7 @@
28
  "@react-sigma/minimap": "^5.0.2",
29
  "@sigma/edge-curve": "^3.1.0",
30
  "@sigma/node-border": "^3.0.0",
 
31
  "class-variance-authority": "^0.7.1",
32
  "clsx": "^2.1.1",
33
  "cmdk": "^1.0.4",
@@ -37,9 +44,13 @@
37
  "minisearch": "^7.1.1",
38
  "react": "^19.0.0",
39
  "react-dom": "^19.0.0",
 
 
40
  "seedrandom": "^3.0.5",
41
  "sigma": "^3.0.1",
 
42
  "tailwind-merge": "^3.0.1",
 
43
  "zustand": "^5.0.3"
44
  },
45
  "devDependencies": {
@@ -47,19 +58,19 @@
47
  "@stylistic/eslint-plugin-js": "^3.1.0",
48
  "@tailwindcss/vite": "^4.0.6",
49
  "@types/bun": "^1.2.2",
50
- "@types/node": "^22.13.1",
51
  "@types/react": "^19.0.8",
52
  "@types/react-dom": "^19.0.3",
53
  "@types/seedrandom": "^3.0.8",
54
  "@vitejs/plugin-react-swc": "^3.8.0",
55
- "eslint": "^9.20.0",
56
  "eslint-config-prettier": "^10.0.1",
57
  "eslint-plugin-react": "^7.37.4",
58
  "eslint-plugin-react-hooks": "^5.1.0",
59
  "eslint-plugin-react-refresh": "^0.4.19",
60
- "globals": "^15.14.0",
61
  "graphology-types": "^0.24.8",
62
- "prettier": "^3.5.0",
63
  "prettier-plugin-tailwindcss": "^0.6.11",
64
  "tailwindcss": "^4.0.6",
65
  "tailwindcss-animate": "^1.0.7",
 
10
  "preview": "bunx --bun vite preview"
11
  },
12
  "dependencies": {
13
+ "@faker-js/faker": "^9.5.0",
14
+ "@radix-ui/react-alert-dialog": "^1.1.6",
15
  "@radix-ui/react-checkbox": "^1.1.4",
16
  "@radix-ui/react-dialog": "^1.1.6",
17
  "@radix-ui/react-popover": "^1.1.6",
18
+ "@radix-ui/react-progress": "^1.1.2",
19
+ "@radix-ui/react-scroll-area": "^1.2.3",
20
+ "@radix-ui/react-select": "^2.1.6",
21
  "@radix-ui/react-separator": "^1.1.2",
22
  "@radix-ui/react-slot": "^1.1.2",
23
+ "@radix-ui/react-tabs": "^1.1.3",
24
  "@radix-ui/react-tooltip": "^1.1.8",
25
+ "@radix-ui/react-use-controllable-state": "^1.1.0",
26
  "@react-sigma/core": "^5.0.2",
27
  "@react-sigma/graph-search": "^5.0.3",
28
  "@react-sigma/layout-circlepack": "^5.0.2",
 
34
  "@react-sigma/minimap": "^5.0.2",
35
  "@sigma/edge-curve": "^3.1.0",
36
  "@sigma/node-border": "^3.0.0",
37
+ "axios": "^1.7.9",
38
  "class-variance-authority": "^0.7.1",
39
  "clsx": "^2.1.1",
40
  "cmdk": "^1.0.4",
 
44
  "minisearch": "^7.1.1",
45
  "react": "^19.0.0",
46
  "react-dom": "^19.0.0",
47
+ "react-dropzone": "^14.3.5",
48
+ "react-number-format": "^5.4.3",
49
  "seedrandom": "^3.0.5",
50
  "sigma": "^3.0.1",
51
+ "sonner": "^1.7.4",
52
  "tailwind-merge": "^3.0.1",
53
+ "tailwind-scrollbar": "^4.0.0",
54
  "zustand": "^5.0.3"
55
  },
56
  "devDependencies": {
 
58
  "@stylistic/eslint-plugin-js": "^3.1.0",
59
  "@tailwindcss/vite": "^4.0.6",
60
  "@types/bun": "^1.2.2",
61
+ "@types/node": "^22.13.4",
62
  "@types/react": "^19.0.8",
63
  "@types/react-dom": "^19.0.3",
64
  "@types/seedrandom": "^3.0.8",
65
  "@vitejs/plugin-react-swc": "^3.8.0",
66
+ "eslint": "^9.20.1",
67
  "eslint-config-prettier": "^10.0.1",
68
  "eslint-plugin-react": "^7.37.4",
69
  "eslint-plugin-react-hooks": "^5.1.0",
70
  "eslint-plugin-react-refresh": "^0.4.19",
71
+ "globals": "^15.15.0",
72
  "graphology-types": "^0.24.8",
73
+ "prettier": "^3.5.1",
74
  "prettier-plugin-tailwindcss": "^0.6.11",
75
  "tailwindcss": "^4.0.6",
76
  "tailwindcss-animate": "^1.0.7",
lightrag_webui/src/App.tsx CHANGED
@@ -1,17 +1,30 @@
1
- import ThemeProvider from '@/components/ThemeProvider'
 
2
  import MessageAlert from '@/components/MessageAlert'
3
- import StatusIndicator from '@/components/StatusIndicator'
4
- import GraphViewer from '@/GraphViewer'
5
  import { healthCheckInterval } from '@/lib/constants'
6
  import { useBackendState } from '@/stores/state'
7
  import { useSettingsStore } from '@/stores/settings'
8
  import { useEffect } from 'react'
 
 
 
 
 
 
 
 
 
 
9
 
10
  function App() {
11
  const message = useBackendState.use.message()
12
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
 
 
13
 
14
- // health check
15
  useEffect(() => {
16
  if (!enableHealthCheck) return
17
 
@@ -24,13 +37,50 @@ function App() {
24
  return () => clearInterval(interval)
25
  }, [enableHealthCheck])
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  return (
28
  <ThemeProvider>
29
- <div className="h-screen w-screen">
30
- <GraphViewer />
31
- </div>
32
- {enableHealthCheck && <StatusIndicator />}
33
- {message !== null && <MessageAlert />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  </ThemeProvider>
35
  )
36
  }
 
1
+ import { useState, useCallback } from 'react'
2
+ import ThemeProvider from '@/components/graph/ThemeProvider'
3
  import MessageAlert from '@/components/MessageAlert'
4
+ import ApiKeyAlert from '@/components/ApiKeyAlert'
5
+ import StatusIndicator from '@/components/graph/StatusIndicator'
6
  import { healthCheckInterval } from '@/lib/constants'
7
  import { useBackendState } from '@/stores/state'
8
  import { useSettingsStore } from '@/stores/settings'
9
  import { useEffect } from 'react'
10
+ import { Toaster } from 'sonner'
11
+ import SiteHeader from '@/features/SiteHeader'
12
+ import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
13
+
14
+ import GraphViewer from '@/features/GraphViewer'
15
+ import DocumentManager from '@/features/DocumentManager'
16
+ import RetrievalTesting from '@/features/RetrievalTesting'
17
+ import ApiSite from '@/features/ApiSite'
18
+
19
+ import { Tabs, TabsContent } from '@/components/ui/Tabs'
20
 
21
  function App() {
22
  const message = useBackendState.use.message()
23
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
24
+ const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
25
+ const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
26
 
27
+ // Health check
28
  useEffect(() => {
29
  if (!enableHealthCheck) return
30
 
 
37
  return () => clearInterval(interval)
38
  }, [enableHealthCheck])
39
 
40
+ const handleTabChange = useCallback(
41
+ (tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
42
+ []
43
+ )
44
+
45
+ useEffect(() => {
46
+ if (message) {
47
+ if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) {
48
+ setApiKeyInvalid(true)
49
+ return
50
+ }
51
+ }
52
+ setApiKeyInvalid(false)
53
+ }, [message, setApiKeyInvalid])
54
+
55
  return (
56
  <ThemeProvider>
57
+ <main className="flex h-screen w-screen overflow-x-hidden">
58
+ <Tabs
59
+ defaultValue={currentTab}
60
+ className="!m-0 flex grow flex-col !p-0"
61
+ onValueChange={handleTabChange}
62
+ >
63
+ <SiteHeader />
64
+ <div className="relative grow">
65
+ <TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
66
+ <DocumentManager />
67
+ </TabsContent>
68
+ <TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
69
+ <GraphViewer />
70
+ </TabsContent>
71
+ <TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
72
+ <RetrievalTesting />
73
+ </TabsContent>
74
+ <TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
75
+ <ApiSite />
76
+ </TabsContent>
77
+ </div>
78
+ </Tabs>
79
+ {enableHealthCheck && <StatusIndicator />}
80
+ {message !== null && !apiKeyInvalid && <MessageAlert />}
81
+ {apiKeyInvalid && <ApiKeyAlert />}
82
+ <Toaster />
83
+ </main>
84
  </ThemeProvider>
85
  )
86
  }
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import { backendBaseUrl } from '@/lib/constants'
2
  import { errorMessage } from '@/lib/utils'
3
  import { useSettingsStore } from '@/stores/settings'
@@ -26,8 +27,6 @@ export type LightragStatus = {
26
  status: 'healthy'
27
  working_directory: string
28
  input_directory: string
29
- indexed_files: string[]
30
- indexed_files_count: number
31
  configuration: {
32
  llm_binding: string
33
  llm_binding_host: string
@@ -51,94 +50,133 @@ export type LightragDocumentsScanProgress = {
51
  progress: number
52
  }
53
 
 
 
 
 
 
 
 
 
54
  export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
55
 
 
 
 
 
 
56
  export type QueryRequest = {
57
  query: string
 
58
  mode: QueryMode
59
- stream?: boolean
60
  only_need_context?: boolean
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
  export type QueryResponse = {
64
  response: string
65
  }
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  export const InvalidApiKeyError = 'Invalid API Key'
68
  export const RequireApiKeError = 'API Key required'
69
 
70
- // Helper functions
71
- const getResponseContent = async (response: Response) => {
72
- const contentType = response.headers.get('content-type')
73
- if (contentType) {
74
- if (contentType.includes('application/json')) {
75
- const data = await response.json()
76
- return JSON.stringify(data, undefined, 2)
77
- } else if (contentType.startsWith('text/')) {
78
- return await response.text()
79
- } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
80
- return await response.text()
81
- } else if (contentType.includes('application/octet-stream')) {
82
- const buffer = await response.arrayBuffer()
83
- const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true })
84
- return decoder.decode(buffer)
85
- } else {
86
- try {
87
- return await response.text()
88
- } catch (error) {
89
- console.warn('Failed to decode as text, may be binary:', error)
90
- return `[Could not decode response body. Content-Type: ${contentType}]`
91
- }
92
- }
93
- } else {
94
- try {
95
- return await response.text()
96
- } catch (error) {
97
- console.warn('Failed to decode as text, may be binary:', error)
98
- return '[Could not decode response body. No Content-Type header.]'
99
- }
100
  }
101
- return ''
102
- }
103
 
104
- const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise<Response> => {
 
105
  const apiKey = useSettingsStore.getState().apiKey
106
- const headers = {
107
- ...(options.headers || {}),
108
- ...(apiKey ? { 'X-API-Key': apiKey } : {})
109
  }
 
 
110
 
111
- const response = await fetch(backendBaseUrl + url, {
112
- ...options,
113
- headers
114
- })
115
-
116
- if (!response.ok) {
117
- throw new Error(
118
- `${response.status} ${response.statusText}\n${await getResponseContent(response)}\n${response.url}`
119
- )
 
 
 
120
  }
121
-
122
- return response
123
- }
124
 
125
  // API methods
126
  export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
127
- const response = await fetchWithAuth(`/graphs?label=${label}`)
128
- return await response.json()
129
  }
130
 
131
  export const getGraphLabels = async (): Promise<string[]> => {
132
- const response = await fetchWithAuth('/graph/label/list')
133
- return await response.json()
134
  }
135
 
136
  export const checkHealth = async (): Promise<
137
  LightragStatus | { status: 'error'; message: string }
138
  > => {
139
  try {
140
- const response = await fetchWithAuth('/health')
141
- return await response.json()
142
  } catch (e) {
143
  return {
144
  status: 'error',
@@ -147,132 +185,132 @@ export const checkHealth = async (): Promise<
147
  }
148
  }
149
 
150
- export const getDocuments = async (): Promise<string[]> => {
151
- const response = await fetchWithAuth('/documents')
152
- return await response.json()
153
  }
154
 
155
- export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
156
- const response = await fetchWithAuth('/documents/scan-progress')
157
- return await response.json()
158
  }
159
 
160
- export const uploadDocument = async (
161
- file: File
162
- ): Promise<{
163
- status: string
164
- message: string
165
- total_documents: number
166
- }> => {
167
- const formData = new FormData()
168
- formData.append('file', file)
169
-
170
- const response = await fetchWithAuth('/documents/upload', {
171
- method: 'POST',
172
- body: formData
173
- })
174
- return await response.json()
175
- }
176
-
177
- export const startDocumentScan = async (): Promise<{ status: string }> => {
178
- const response = await fetchWithAuth('/documents/scan', {
179
- method: 'POST'
180
- })
181
- return await response.json()
182
  }
183
 
184
  export const queryText = async (request: QueryRequest): Promise<QueryResponse> => {
185
- const response = await fetchWithAuth('/query', {
186
- method: 'POST',
187
- headers: {
188
- 'Content-Type': 'application/json'
189
- },
190
- body: JSON.stringify(request)
191
- })
192
- return await response.json()
193
  }
194
 
195
- export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => {
196
- const response = await fetchWithAuth('/query/stream', {
197
- method: 'POST',
198
- headers: {
199
- 'Content-Type': 'application/json'
200
- },
201
- body: JSON.stringify(request)
202
- })
 
 
 
 
 
 
 
 
 
 
 
203
 
204
- const reader = response.body?.getReader()
205
- if (!reader) throw new Error('No response body')
206
-
207
- const decoder = new TextDecoder()
208
- while (true) {
209
- const { done, value } = await reader.read()
210
- if (done) break
211
-
212
- const chunk = decoder.decode(value)
213
- const lines = chunk.split('\n')
214
- for (const line of lines) {
215
- if (line) {
216
- try {
217
- const data = JSON.parse(line)
218
- if (data.response) {
219
- onChunk(data.response)
220
  }
221
- } catch (e) {
222
- console.error('Error parsing stream chunk:', e)
223
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  }
225
  }
 
 
 
 
226
  }
227
  }
228
 
229
- // Text insertion API
230
  export const insertText = async (
231
  text: string,
232
  description?: string
233
- ): Promise<{
234
- status: string
235
- message: string
236
- document_count: number
237
- }> => {
238
- const response = await fetchWithAuth('/documents/text', {
239
- method: 'POST',
240
- headers: {
241
- 'Content-Type': 'application/json'
242
- },
243
- body: JSON.stringify({ text, description })
244
- })
245
- return await response.json()
246
  }
247
 
248
- // Batch file upload API
249
- export const uploadBatchDocuments = async (
250
- files: File[]
251
- ): Promise<{
252
- status: string
253
- message: string
254
- document_count: number
255
- }> => {
256
  const formData = new FormData()
257
- files.forEach((file) => {
258
- formData.append('files', file)
259
- })
260
 
261
- const response = await fetchWithAuth('/documents/batch', {
262
- method: 'POST',
263
- body: formData
 
 
 
 
 
 
 
 
 
264
  })
265
- return await response.json()
266
  }
267
 
268
- // Clear all documents API
269
- export const clearDocuments = async (): Promise<{
270
- status: string
271
- message: string
272
- document_count: number
273
- }> => {
274
- const response = await fetchWithAuth('/documents', {
275
- method: 'DELETE'
276
- })
277
- return await response.json()
 
 
 
 
 
 
278
  }
 
1
+ import axios, { AxiosError } from 'axios'
2
  import { backendBaseUrl } from '@/lib/constants'
3
  import { errorMessage } from '@/lib/utils'
4
  import { useSettingsStore } from '@/stores/settings'
 
27
  status: 'healthy'
28
  working_directory: string
29
  input_directory: string
 
 
30
  configuration: {
31
  llm_binding: string
32
  llm_binding_host: string
 
50
  progress: number
51
  }
52
 
53
+ /**
54
+ * Specifies the retrieval mode:
55
+ * - "naive": Performs a basic search without advanced techniques.
56
+ * - "local": Focuses on context-dependent information.
57
+ * - "global": Utilizes global knowledge.
58
+ * - "hybrid": Combines local and global retrieval methods.
59
+ * - "mix": Integrates knowledge graph and vector retrieval.
60
+ */
61
  export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
62
 
63
+ export type Message = {
64
+ role: 'user' | 'assistant' | 'system'
65
+ content: string
66
+ }
67
+
68
  export type QueryRequest = {
69
  query: string
70
+ /** Specifies the retrieval mode. */
71
  mode: QueryMode
72
+ /** If True, only returns the retrieved context without generating a response. */
73
  only_need_context?: boolean
74
+ /** If True, only returns the generated prompt without producing a response. */
75
+ only_need_prompt?: boolean
76
+ /** Defines the response format. Examples: 'Multiple Paragraphs', 'Single Paragraph', 'Bullet Points'. */
77
+ response_type?: string
78
+ /** If True, enables streaming output for real-time responses. */
79
+ stream?: boolean
80
+ /** Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode. */
81
+ top_k?: number
82
+ /** Maximum number of tokens allowed for each retrieved text chunk. */
83
+ max_token_for_text_unit?: number
84
+ /** Maximum number of tokens allocated for relationship descriptions in global retrieval. */
85
+ max_token_for_global_context?: number
86
+ /** Maximum number of tokens allocated for entity descriptions in local retrieval. */
87
+ max_token_for_local_context?: number
88
+ /** List of high-level keywords to prioritize in retrieval. */
89
+ hl_keywords?: string[]
90
+ /** List of low-level keywords to refine retrieval focus. */
91
+ ll_keywords?: string[]
92
+ /**
93
+ * Stores past conversation history to maintain context.
94
+ * Format: [{"role": "user/assistant", "content": "message"}].
95
+ */
96
+ conversation_history?: Message[]
97
+ /** Number of complete conversation turns (user-assistant pairs) to consider in the response context. */
98
+ history_turns?: number
99
  }
100
 
101
  export type QueryResponse = {
102
  response: string
103
  }
104
 
105
+ export type DocActionResponse = {
106
+ status: 'success' | 'partial_success' | 'failure'
107
+ message: string
108
+ }
109
+
110
+ export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed'
111
+
112
+ export type DocStatusResponse = {
113
+ id: string
114
+ content_summary: string
115
+ content_length: number
116
+ status: DocStatus
117
+ created_at: string
118
+ updated_at: string
119
+ chunks_count?: number
120
+ error?: string
121
+ metadata?: Record<string, any>
122
+ }
123
+
124
+ export type DocsStatusesResponse = {
125
+ statuses: Record<DocStatus, DocStatusResponse[]>
126
+ }
127
+
128
  export const InvalidApiKeyError = 'Invalid API Key'
129
  export const RequireApiKeError = 'API Key required'
130
 
131
+ // Axios instance
132
+ const axiosInstance = axios.create({
133
+ baseURL: backendBaseUrl,
134
+ headers: {
135
+ 'Content-Type': 'application/json'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
+ })
 
138
 
139
+ // Interceptor:add api key
140
+ axiosInstance.interceptors.request.use((config) => {
141
  const apiKey = useSettingsStore.getState().apiKey
142
+ if (apiKey) {
143
+ config.headers['X-API-Key'] = apiKey
 
144
  }
145
+ return config
146
+ })
147
 
148
+ // Interceptor:hanle error
149
+ axiosInstance.interceptors.response.use(
150
+ (response) => response,
151
+ (error: AxiosError) => {
152
+ if (error.response) {
153
+ throw new Error(
154
+ `${error.response.status} ${error.response.statusText}\n${JSON.stringify(
155
+ error.response.data
156
+ )}\n${error.config?.url}`
157
+ )
158
+ }
159
+ throw error
160
  }
161
+ )
 
 
162
 
163
  // API methods
164
  export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
165
+ const response = await axiosInstance.get(`/graphs?label=${label}`)
166
+ return response.data
167
  }
168
 
169
  export const getGraphLabels = async (): Promise<string[]> => {
170
+ const response = await axiosInstance.get('/graph/label/list')
171
+ return response.data
172
  }
173
 
174
  export const checkHealth = async (): Promise<
175
  LightragStatus | { status: 'error'; message: string }
176
  > => {
177
  try {
178
+ const response = await axiosInstance.get('/health')
179
+ return response.data
180
  } catch (e) {
181
  return {
182
  status: 'error',
 
185
  }
186
  }
187
 
188
+ export const getDocuments = async (): Promise<DocsStatusesResponse> => {
189
+ const response = await axiosInstance.get('/documents')
190
+ return response.data
191
  }
192
 
193
+ export const scanNewDocuments = async (): Promise<{ status: string }> => {
194
+ const response = await axiosInstance.post('/documents/scan')
195
+ return response.data
196
  }
197
 
198
+ export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
199
+ const response = await axiosInstance.get('/documents/scan-progress')
200
+ return response.data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
202
 
203
  export const queryText = async (request: QueryRequest): Promise<QueryResponse> => {
204
+ const response = await axiosInstance.post('/query', request)
205
+ return response.data
 
 
 
 
 
 
206
  }
207
 
208
+ export const queryTextStream = async (
209
+ request: QueryRequest,
210
+ onChunk: (chunk: string) => void,
211
+ onError?: (error: string) => void
212
+ ) => {
213
+ try {
214
+ let buffer = ''
215
+ await axiosInstance.post('/query/stream', request, {
216
+ responseType: 'text',
217
+ headers: {
218
+ Accept: 'application/x-ndjson'
219
+ },
220
+ transformResponse: [
221
+ (data: string) => {
222
+ // Accumulate the data and process complete lines
223
+ buffer += data
224
+ const lines = buffer.split('\n')
225
+ // Keep the last potentially incomplete line in the buffer
226
+ buffer = lines.pop() || ''
227
 
228
+ for (const line of lines) {
229
+ if (line.trim()) {
230
+ try {
231
+ const parsed = JSON.parse(line)
232
+ if (parsed.response) {
233
+ onChunk(parsed.response)
234
+ } else if (parsed.error && onError) {
235
+ onError(parsed.error)
236
+ }
237
+ } catch (e) {
238
+ console.error('Error parsing stream chunk:', e)
239
+ if (onError) onError('Error parsing server response')
240
+ }
241
+ }
 
 
242
  }
243
+ return data
 
244
  }
245
+ ]
246
+ })
247
+
248
+ // Process any remaining data in the buffer
249
+ if (buffer.trim()) {
250
+ try {
251
+ const parsed = JSON.parse(buffer)
252
+ if (parsed.response) {
253
+ onChunk(parsed.response)
254
+ } else if (parsed.error && onError) {
255
+ onError(parsed.error)
256
+ }
257
+ } catch (e) {
258
+ console.error('Error parsing final chunk:', e)
259
+ if (onError) onError('Error parsing server response')
260
  }
261
  }
262
+ } catch (error) {
263
+ const message = errorMessage(error)
264
+ console.error('Stream request failed:', message)
265
+ if (onError) onError(message)
266
  }
267
  }
268
 
 
269
  export const insertText = async (
270
  text: string,
271
  description?: string
272
+ ): Promise<DocActionResponse> => {
273
+ const response = await axiosInstance.post('/documents/text', { text, description })
274
+ return response.data
 
 
 
 
 
 
 
 
 
 
275
  }
276
 
277
+ export const uploadDocument = async (
278
+ file: File,
279
+ onUploadProgress?: (percentCompleted: number) => void
280
+ ): Promise<DocActionResponse> => {
 
 
 
 
281
  const formData = new FormData()
282
+ formData.append('file', file)
 
 
283
 
284
+ const response = await axiosInstance.post('/documents/upload', formData, {
285
+ headers: {
286
+ 'Content-Type': 'multipart/form-data'
287
+ },
288
+ // prettier-ignore
289
+ onUploadProgress:
290
+ onUploadProgress !== undefined
291
+ ? (progressEvent) => {
292
+ const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total!)
293
+ onUploadProgress(percentCompleted)
294
+ }
295
+ : undefined
296
  })
297
+ return response.data
298
  }
299
 
300
+ export const batchUploadDocuments = async (
301
+ files: File[],
302
+ onUploadProgress?: (fileName: string, percentCompleted: number) => void
303
+ ): Promise<DocActionResponse[]> => {
304
+ return await Promise.all(
305
+ files.map(async (file) => {
306
+ return await uploadDocument(file, (percentCompleted) => {
307
+ onUploadProgress?.(file.name, percentCompleted)
308
+ })
309
+ })
310
+ )
311
+ }
312
+
313
+ export const clearDocuments = async (): Promise<DocActionResponse> => {
314
+ const response = await axiosInstance.delete('/documents')
315
+ return response.data
316
  }
lightrag_webui/src/components/ApiKeyAlert.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from 'react'
2
+ import {
3
+ AlertDialog,
4
+ AlertDialogContent,
5
+ AlertDialogDescription,
6
+ AlertDialogHeader,
7
+ AlertDialogTitle
8
+ } from '@/components/ui/AlertDialog'
9
+ import Button from '@/components/ui/Button'
10
+ import Input from '@/components/ui/Input'
11
+ import { useSettingsStore } from '@/stores/settings'
12
+ import { useBackendState } from '@/stores/state'
13
+ import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
14
+
15
+ import { toast } from 'sonner'
16
+
17
+ const ApiKeyAlert = () => {
18
+ const [opened, setOpened] = useState<boolean>(true)
19
+ const apiKey = useSettingsStore.use.apiKey()
20
+ const [tempApiKey, setTempApiKey] = useState<string>('')
21
+ const message = useBackendState.use.message()
22
+
23
+ useEffect(() => {
24
+ setTempApiKey(apiKey || '')
25
+ }, [apiKey, opened])
26
+
27
+ useEffect(() => {
28
+ if (message) {
29
+ if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) {
30
+ setOpened(true)
31
+ }
32
+ }
33
+ }, [message, setOpened])
34
+
35
+ const setApiKey = useCallback(async () => {
36
+ useSettingsStore.setState({ apiKey: tempApiKey || null })
37
+ if (await useBackendState.getState().check()) {
38
+ setOpened(false)
39
+ return
40
+ }
41
+ toast.error('API Key is invalid')
42
+ }, [tempApiKey])
43
+
44
+ const handleTempApiKeyChange = useCallback(
45
+ (e: React.ChangeEvent<HTMLInputElement>) => {
46
+ setTempApiKey(e.target.value)
47
+ },
48
+ [setTempApiKey]
49
+ )
50
+
51
+ return (
52
+ <AlertDialog open={opened} onOpenChange={setOpened}>
53
+ <AlertDialogContent>
54
+ <AlertDialogHeader>
55
+ <AlertDialogTitle>API Key is required</AlertDialogTitle>
56
+ <AlertDialogDescription>Please enter your API key</AlertDialogDescription>
57
+ </AlertDialogHeader>
58
+ <form className="flex gap-2" onSubmit={(e) => e.preventDefault()}>
59
+ <Input
60
+ type="password"
61
+ value={tempApiKey}
62
+ onChange={handleTempApiKeyChange}
63
+ placeholder="Enter your API key"
64
+ className="max-h-full w-full min-w-0"
65
+ autoComplete="off"
66
+ />
67
+
68
+ <Button onClick={setApiKey} variant="outline" size="sm">
69
+ Save
70
+ </Button>
71
+ </form>
72
+ </AlertDialogContent>
73
+ </AlertDialog>
74
+ )
75
+ }
76
+
77
+ export default ApiKeyAlert
lightrag_webui/src/components/MessageAlert.tsx CHANGED
@@ -22,10 +22,11 @@ const MessageAlert = () => {
22
 
23
  return (
24
  <Alert
25
- variant={health ? 'default' : 'destructive'}
26
  className={cn(
27
- 'bg-background/90 absolute top-2 left-1/2 flex w-auto -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
28
- isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0'
 
29
  )}
30
  >
31
  {!health && (
@@ -42,7 +43,7 @@ const MessageAlert = () => {
42
  <Button
43
  size="sm"
44
  variant={controlButtonVariant}
45
- className="text-primary max-h-8 border !p-2 text-xs"
46
  onClick={() => useBackendState.getState().clear()}
47
  >
48
  Close
 
22
 
23
  return (
24
  <Alert
25
+ // variant={health ? 'default' : 'destructive'}
26
  className={cn(
27
+ 'bg-background/90 absolute top-12 left-1/2 flex w-auto max-w-lg -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
28
+ isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0',
29
+ !health && 'bg-red-700 text-white'
30
  )}
31
  >
32
  {!health && (
 
43
  <Button
44
  size="sm"
45
  variant={controlButtonVariant}
46
+ className="border-primary max-h-8 border !p-2 text-xs"
47
  onClick={() => useBackendState.getState().clear()}
48
  >
49
  Close
lightrag_webui/src/components/documents/ClearDocumentsDialog.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react'
2
+ import Button from '@/components/ui/Button'
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogTrigger
10
+ } from '@/components/ui/Dialog'
11
+ import { toast } from 'sonner'
12
+ import { errorMessage } from '@/lib/utils'
13
+ import { clearDocuments } from '@/api/lightrag'
14
+
15
+ import { EraserIcon } from 'lucide-react'
16
+
17
+ export default function ClearDocumentsDialog() {
18
+ const [open, setOpen] = useState(false)
19
+
20
+ const handleClear = useCallback(async () => {
21
+ try {
22
+ const result = await clearDocuments()
23
+ if (result.status === 'success') {
24
+ toast.success('Documents cleared successfully')
25
+ setOpen(false)
26
+ } else {
27
+ toast.error(`Clear Documents Failed:\n${result.message}`)
28
+ }
29
+ } catch (err) {
30
+ toast.error('Clear Documents Failed:\n' + errorMessage(err))
31
+ }
32
+ }, [setOpen])
33
+
34
+ return (
35
+ <Dialog open={open} onOpenChange={setOpen}>
36
+ <DialogTrigger asChild>
37
+ <Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
38
+ <EraserIcon/> Clear
39
+ </Button>
40
+ </DialogTrigger>
41
+ <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
42
+ <DialogHeader>
43
+ <DialogTitle>Clear documents</DialogTitle>
44
+ <DialogDescription>Do you really want to clear all documents?</DialogDescription>
45
+ </DialogHeader>
46
+ <Button variant="destructive" onClick={handleClear}>
47
+ YES
48
+ </Button>
49
+ </DialogContent>
50
+ </Dialog>
51
+ )
52
+ }
lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react'
2
+ import Button from '@/components/ui/Button'
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogTrigger
10
+ } from '@/components/ui/Dialog'
11
+ import FileUploader from '@/components/ui/FileUploader'
12
+ import { toast } from 'sonner'
13
+ import { errorMessage } from '@/lib/utils'
14
+ import { uploadDocument } from '@/api/lightrag'
15
+
16
+ import { UploadIcon } from 'lucide-react'
17
+
18
+ export default function UploadDocumentsDialog() {
19
+ const [open, setOpen] = useState(false)
20
+ const [isUploading, setIsUploading] = useState(false)
21
+ const [progresses, setProgresses] = useState<Record<string, number>>({})
22
+
23
+ const handleDocumentsUpload = useCallback(
24
+ async (filesToUpload: File[]) => {
25
+ setIsUploading(true)
26
+
27
+ try {
28
+ await Promise.all(
29
+ filesToUpload.map(async (file) => {
30
+ try {
31
+ const result = await uploadDocument(file, (percentCompleted: number) => {
32
+ console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
33
+ setProgresses((pre) => ({
34
+ ...pre,
35
+ [file.name]: percentCompleted
36
+ }))
37
+ })
38
+ if (result.status === 'success') {
39
+ toast.success(`Upload Success:\n${file.name} uploaded successfully`)
40
+ } else {
41
+ toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
42
+ }
43
+ } catch (err) {
44
+ toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
45
+ }
46
+ })
47
+ )
48
+ } catch (err) {
49
+ toast.error('Upload Failed\n' + errorMessage(err))
50
+ } finally {
51
+ setIsUploading(false)
52
+ // setOpen(false)
53
+ }
54
+ },
55
+ [setIsUploading, setProgresses]
56
+ )
57
+
58
+ return (
59
+ <Dialog
60
+ open={open}
61
+ onOpenChange={(open) => {
62
+ if (isUploading && !open) {
63
+ return
64
+ }
65
+ setOpen(open)
66
+ }}
67
+ >
68
+ <DialogTrigger asChild>
69
+ <Button variant="default" side="bottom" tooltip="Upload documents" size="sm">
70
+ <UploadIcon /> Upload
71
+ </Button>
72
+ </DialogTrigger>
73
+ <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
74
+ <DialogHeader>
75
+ <DialogTitle>Upload documents</DialogTitle>
76
+ <DialogDescription>
77
+ Drag and drop your documents here or click to browse.
78
+ </DialogDescription>
79
+ </DialogHeader>
80
+ <FileUploader
81
+ maxFileCount={Infinity}
82
+ maxSize={200 * 1024 * 1024}
83
+ description="supported types: TXT, MD, DOC, PDF, PPTX"
84
+ onUpload={handleDocumentsUpload}
85
+ progresses={progresses}
86
+ disabled={isUploading}
87
+ />
88
+ </DialogContent>
89
+ </Dialog>
90
+ )
91
+ }
lightrag_webui/src/components/{FocusOnNode.tsx → graph/FocusOnNode.tsx} RENAMED
File without changes
lightrag_webui/src/components/{FullScreenControl.tsx → graph/FullScreenControl.tsx} RENAMED
File without changes
lightrag_webui/src/components/{GraphControl.tsx → graph/GraphControl.tsx} RENAMED
File without changes
lightrag_webui/src/components/{GraphLabels.tsx → graph/GraphLabels.tsx} RENAMED
File without changes
lightrag_webui/src/components/{GraphSearch.tsx → graph/GraphSearch.tsx} RENAMED
File without changes
lightrag_webui/src/components/{LayoutsControl.tsx → graph/LayoutsControl.tsx} RENAMED
File without changes
lightrag_webui/src/components/{PropertiesView.tsx → graph/PropertiesView.tsx} RENAMED
File without changes
lightrag_webui/src/components/{Settings.tsx → graph/Settings.tsx} RENAMED
@@ -1,5 +1,5 @@
1
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
2
- import { Checkbox } from '@/components/ui/Checkbox'
3
  import Button from '@/components/ui/Button'
4
  import Separator from '@/components/ui/Separator'
5
  import Input from '@/components/ui/Input'
@@ -40,7 +40,7 @@ const LabeledCheckBox = ({
40
  */
41
  export default function Settings() {
42
  const [opened, setOpened] = useState<boolean>(false)
43
- const [tempApiKey, setTempApiKey] = useState<string>('') // 用于临时存储输入的API Key
44
 
45
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
46
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
 
1
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
2
+ import Checkbox from '@/components/ui/Checkbox'
3
  import Button from '@/components/ui/Button'
4
  import Separator from '@/components/ui/Separator'
5
  import Input from '@/components/ui/Input'
 
40
  */
41
  export default function Settings() {
42
  const [opened, setOpened] = useState<boolean>(false)
43
+ const [tempApiKey, setTempApiKey] = useState<string>('')
44
 
45
  const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
46
  const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
lightrag_webui/src/components/{StatusCard.tsx → graph/StatusCard.tsx} RENAMED
@@ -14,8 +14,6 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => {
14
  <span className="truncate">{status.working_directory}</span>
15
  <span>Input Directory:</span>
16
  <span className="truncate">{status.input_directory}</span>
17
- <span>Indexed Files:</span>
18
- <span>{status.indexed_files_count}</span>
19
  </div>
20
  </div>
21
 
 
14
  <span className="truncate">{status.working_directory}</span>
15
  <span>Input Directory:</span>
16
  <span className="truncate">{status.input_directory}</span>
 
 
17
  </div>
18
  </div>
19
 
lightrag_webui/src/components/{StatusIndicator.tsx → graph/StatusIndicator.tsx} RENAMED
@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'
2
  import { useBackendState } from '@/stores/state'
3
  import { useEffect, useState } from 'react'
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
5
- import StatusCard from '@/components/StatusCard'
6
 
7
  const StatusIndicator = () => {
8
  const health = useBackendState.use.health()
 
2
  import { useBackendState } from '@/stores/state'
3
  import { useEffect, useState } from 'react'
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
5
+ import StatusCard from '@/components/graph/StatusCard'
6
 
7
  const StatusIndicator = () => {
8
  const health = useBackendState.use.health()
lightrag_webui/src/components/{ThemeProvider.tsx → graph/ThemeProvider.tsx} RENAMED
File without changes
lightrag_webui/src/components/{ThemeToggle.tsx → graph/ThemeToggle.tsx} RENAMED
@@ -19,6 +19,7 @@ export default function ThemeToggle() {
19
  variant={controlButtonVariant}
20
  tooltip="Switch to light theme"
21
  size="icon"
 
22
  >
23
  <MoonIcon />
24
  </Button>
@@ -30,6 +31,7 @@ export default function ThemeToggle() {
30
  variant={controlButtonVariant}
31
  tooltip="Switch to dark theme"
32
  size="icon"
 
33
  >
34
  <SunIcon />
35
  </Button>
 
19
  variant={controlButtonVariant}
20
  tooltip="Switch to light theme"
21
  size="icon"
22
+ side="bottom"
23
  >
24
  <MoonIcon />
25
  </Button>
 
31
  variant={controlButtonVariant}
32
  tooltip="Switch to dark theme"
33
  size="icon"
34
+ side="bottom"
35
  >
36
  <SunIcon />
37
  </Button>
lightrag_webui/src/components/{ZoomControl.tsx → graph/ZoomControl.tsx} RENAMED
File without changes
lightrag_webui/src/components/retrieval/QuerySettings.tsx ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback } from 'react'
2
+ import { QueryMode, QueryRequest } from '@/api/lightrag'
3
+ import Text from '@/components/ui/Text'
4
+ import Input from '@/components/ui/Input'
5
+ import Checkbox from '@/components/ui/Checkbox'
6
+ import NumberInput from '@/components/ui/NumberInput'
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectGroup,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue
15
+ } from '@/components/ui/Select'
16
+ import { useSettingsStore } from '@/stores/settings'
17
+
18
+ export default function QuerySettings() {
19
+ const querySettings = useSettingsStore((state) => state.querySettings)
20
+
21
+ const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
22
+ useSettingsStore.getState().updateQuerySettings({ [key]: value })
23
+ }, [])
24
+
25
+ return (
26
+ <Card className="flex shrink-0 flex-col">
27
+ <CardHeader className="px-4 pt-4 pb-2">
28
+ <CardTitle>Parameters</CardTitle>
29
+ <CardDescription>Configure your query parameters</CardDescription>
30
+ </CardHeader>
31
+ <CardContent className="m-0 flex grow flex-col p-0 text-xs">
32
+ <div className="relative size-full">
33
+ <div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2">
34
+ {/* Query Mode */}
35
+ <>
36
+ <Text
37
+ className="ml-1"
38
+ text="Query Mode"
39
+ tooltip="Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval"
40
+ side="left"
41
+ />
42
+ <Select
43
+ value={querySettings.mode}
44
+ onValueChange={(v) => handleChange('mode', v as QueryMode)}
45
+ >
46
+ <SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
47
+ <SelectValue />
48
+ </SelectTrigger>
49
+ <SelectContent>
50
+ <SelectGroup>
51
+ <SelectItem value="naive">Naive</SelectItem>
52
+ <SelectItem value="local">Local</SelectItem>
53
+ <SelectItem value="global">Global</SelectItem>
54
+ <SelectItem value="hybrid">Hybrid</SelectItem>
55
+ <SelectItem value="mix">Mix</SelectItem>
56
+ </SelectGroup>
57
+ </SelectContent>
58
+ </Select>
59
+ </>
60
+
61
+ {/* Response Format */}
62
+ <>
63
+ <Text
64
+ className="ml-1"
65
+ text="Response Format"
66
+ tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
67
+ side="left"
68
+ />
69
+ <Select
70
+ value={querySettings.response_type}
71
+ onValueChange={(v) => handleChange('response_type', v)}
72
+ >
73
+ <SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
74
+ <SelectValue />
75
+ </SelectTrigger>
76
+ <SelectContent>
77
+ <SelectGroup>
78
+ <SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem>
79
+ <SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
80
+ <SelectItem value="Bullet Points">Bullet Points</SelectItem>
81
+ </SelectGroup>
82
+ </SelectContent>
83
+ </Select>
84
+ </>
85
+
86
+ {/* Top K */}
87
+ <>
88
+ <Text
89
+ className="ml-1"
90
+ text="Top K Results"
91
+ tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
92
+ side="left"
93
+ />
94
+ <NumberInput
95
+ id="top_k"
96
+ stepper={1}
97
+ value={querySettings.top_k}
98
+ onValueChange={(v) => handleChange('top_k', v)}
99
+ min={1}
100
+ placeholder="Number of results"
101
+ />
102
+ </>
103
+
104
+ {/* Max Tokens */}
105
+ <>
106
+ <>
107
+ <Text
108
+ className="ml-1"
109
+ text="Max Tokens for Text Unit"
110
+ tooltip="Maximum number of tokens allowed for each retrieved text chunk"
111
+ side="left"
112
+ />
113
+ <NumberInput
114
+ id="max_token_for_text_unit"
115
+ stepper={500}
116
+ value={querySettings.max_token_for_text_unit}
117
+ onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
118
+ min={1}
119
+ placeholder="Max tokens for text unit"
120
+ />
121
+ </>
122
+
123
+ <>
124
+ <Text
125
+ text="Max Tokens for Global Context"
126
+ tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
127
+ side="left"
128
+ />
129
+ <NumberInput
130
+ id="max_token_for_global_context"
131
+ stepper={500}
132
+ value={querySettings.max_token_for_global_context}
133
+ onValueChange={(v) => handleChange('max_token_for_global_context', v)}
134
+ min={1}
135
+ placeholder="Max tokens for global context"
136
+ />
137
+ </>
138
+
139
+ <>
140
+ <Text
141
+ className="ml-1"
142
+ text="Max Tokens for Local Context"
143
+ tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
144
+ side="left"
145
+ />
146
+ <NumberInput
147
+ id="max_token_for_local_context"
148
+ stepper={500}
149
+ value={querySettings.max_token_for_local_context}
150
+ onValueChange={(v) => handleChange('max_token_for_local_context', v)}
151
+ min={1}
152
+ placeholder="Max tokens for local context"
153
+ />
154
+ </>
155
+ </>
156
+
157
+ {/* History Turns */}
158
+ <>
159
+ <Text
160
+ className="ml-1"
161
+ text="History Turns"
162
+ tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
163
+ side="left"
164
+ />
165
+ <NumberInput
166
+ className="!border-input"
167
+ id="history_turns"
168
+ stepper={1}
169
+ type="text"
170
+ value={querySettings.history_turns}
171
+ onValueChange={(v) => handleChange('history_turns', v)}
172
+ min={0}
173
+ placeholder="Number of history turns"
174
+ />
175
+ </>
176
+
177
+ {/* Keywords */}
178
+ <>
179
+ <>
180
+ <Text
181
+ className="ml-1"
182
+ text="High-Level Keywords"
183
+ tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
184
+ side="left"
185
+ />
186
+ <Input
187
+ id="hl_keywords"
188
+ type="text"
189
+ value={querySettings.hl_keywords?.join(', ')}
190
+ onChange={(e) => {
191
+ const keywords = e.target.value
192
+ .split(',')
193
+ .map((k) => k.trim())
194
+ .filter((k) => k !== '')
195
+ handleChange('hl_keywords', keywords)
196
+ }}
197
+ placeholder="Enter keywords"
198
+ />
199
+ </>
200
+
201
+ <>
202
+ <Text
203
+ className="ml-1"
204
+ text="Low-Level Keywords"
205
+ tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
206
+ side="left"
207
+ />
208
+ <Input
209
+ id="ll_keywords"
210
+ type="text"
211
+ value={querySettings.ll_keywords?.join(', ')}
212
+ onChange={(e) => {
213
+ const keywords = e.target.value
214
+ .split(',')
215
+ .map((k) => k.trim())
216
+ .filter((k) => k !== '')
217
+ handleChange('ll_keywords', keywords)
218
+ }}
219
+ placeholder="Enter keywords"
220
+ />
221
+ </>
222
+ </>
223
+
224
+ {/* Toggle Options */}
225
+ <>
226
+ <div className="flex items-center gap-2">
227
+ <Text
228
+ className="ml-1"
229
+ text="Only Need Context"
230
+ tooltip="If True, only returns the retrieved context without generating a response"
231
+ side="left"
232
+ />
233
+ <div className="grow" />
234
+ <Checkbox
235
+ className="mr-1 cursor-pointer"
236
+ id="only_need_context"
237
+ checked={querySettings.only_need_context}
238
+ onCheckedChange={(checked) => handleChange('only_need_context', checked)}
239
+ />
240
+ </div>
241
+
242
+ <div className="flex items-center gap-2">
243
+ <Text
244
+ className="ml-1"
245
+ text="Only Need Prompt"
246
+ tooltip="If True, only returns the generated prompt without producing a response"
247
+ side="left"
248
+ />
249
+ <div className="grow" />
250
+ <Checkbox
251
+ className="mr-1 cursor-pointer"
252
+ id="only_need_prompt"
253
+ checked={querySettings.only_need_prompt}
254
+ onCheckedChange={(checked) => handleChange('only_need_prompt', checked)}
255
+ />
256
+ </div>
257
+
258
+ <div className="flex items-center gap-2">
259
+ <Text
260
+ className="ml-1"
261
+ text="Stream Response"
262
+ tooltip="If True, enables streaming output for real-time responses"
263
+ side="left"
264
+ />
265
+ <div className="grow" />
266
+ <Checkbox
267
+ className="mr-1 cursor-pointer"
268
+ id="stream"
269
+ checked={querySettings.stream}
270
+ onCheckedChange={(checked) => handleChange('stream', checked)}
271
+ />
272
+ </div>
273
+ </>
274
+ </div>
275
+ </div>
276
+ </CardContent>
277
+ </Card>
278
+ )
279
+ }
lightrag_webui/src/components/ui/AlertDialog.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
3
+
4
+ import { cn } from '@/lib/utils'
5
+ import { buttonVariants } from '@/components/ui/Button'
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
20
+ className
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ))
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ComponentRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ 'bg-background 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-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ))
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45
+
46
+ const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
47
+ <div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
48
+ )
49
+ AlertDialogHeader.displayName = 'AlertDialogHeader'
50
+
51
+ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
54
+ {...props}
55
+ />
56
+ )
57
+ AlertDialogFooter.displayName = 'AlertDialogFooter'
58
+
59
+ const AlertDialogTitle = React.forwardRef<
60
+ React.ComponentRef<typeof AlertDialogPrimitive.Title>,
61
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
62
+ >(({ className, ...props }, ref) => (
63
+ <AlertDialogPrimitive.Title
64
+ ref={ref}
65
+ className={cn('text-lg font-semibold', className)}
66
+ {...props}
67
+ />
68
+ ))
69
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
70
+
71
+ const AlertDialogDescription = React.forwardRef<
72
+ React.ComponentRef<typeof AlertDialogPrimitive.Description>,
73
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
74
+ >(({ className, ...props }, ref) => (
75
+ <AlertDialogPrimitive.Description
76
+ ref={ref}
77
+ className={cn('text-muted-foreground text-sm', className)}
78
+ {...props}
79
+ />
80
+ ))
81
+ AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
82
+
83
+ const AlertDialogAction = React.forwardRef<
84
+ React.ComponentRef<typeof AlertDialogPrimitive.Action>,
85
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
86
+ >(({ className, ...props }, ref) => (
87
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
88
+ ))
89
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
90
+
91
+ const AlertDialogCancel = React.forwardRef<
92
+ React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,
93
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
94
+ >(({ className, ...props }, ref) => (
95
+ <AlertDialogPrimitive.Cancel
96
+ ref={ref}
97
+ className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
98
+ {...props}
99
+ />
100
+ ))
101
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
102
+
103
+ export {
104
+ AlertDialog,
105
+ AlertDialogPortal,
106
+ AlertDialogOverlay,
107
+ AlertDialogTrigger,
108
+ AlertDialogContent,
109
+ AlertDialogHeader,
110
+ AlertDialogFooter,
111
+ AlertDialogTitle,
112
+ AlertDialogDescription,
113
+ AlertDialogAction,
114
+ AlertDialogCancel
115
+ }
lightrag_webui/src/components/ui/AsyncSearch.tsx CHANGED
@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
193
  </div>
194
  )}
195
  </div>
196
- <CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
197
  {error && <div className="text-destructive p-4 text-center">{error}</div>}
198
  {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
199
  {!loading &&
 
193
  </div>
194
  )}
195
  </div>
196
+ <CommandList hidden={!open || debouncedSearchTerm.length === 0}>
197
  {error && <div className="text-destructive p-4 text-center">{error}</div>}
198
  {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
199
  {!loading &&
lightrag_webui/src/components/ui/Badge.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const badgeVariants = cva(
7
+ 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
12
+ secondary:
13
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
14
+ destructive:
15
+ 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
16
+ outline: 'text-foreground'
17
+ }
18
+ },
19
+ defaultVariants: {
20
+ variant: 'default'
21
+ }
22
+ }
23
+ )
24
+
25
+ export interface BadgeProps
26
+ extends React.HTMLAttributes<HTMLDivElement>,
27
+ VariantProps<typeof badgeVariants> {}
28
+
29
+ function Badge({ className, variant, ...props }: BadgeProps) {
30
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />
31
+ }
32
+
33
+ export default Badge
lightrag_webui/src/components/ui/Button.tsx CHANGED
@@ -4,7 +4,8 @@ import { cva, type VariantProps } from 'class-variance-authority'
4
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
5
  import { cn } from '@/lib/utils'
6
 
7
- const buttonVariants = cva(
 
8
  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
  {
10
  variants: {
 
4
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
5
  import { cn } from '@/lib/utils'
6
 
7
+ // eslint-disable-next-line react-refresh/only-export-components
8
+ export const buttonVariants = cva(
9
  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
10
  {
11
  variants: {
lightrag_webui/src/components/ui/Card.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div
8
+ ref={ref}
9
+ className={cn('bg-card text-card-foreground rounded-xl border shadow', className)}
10
+ {...props}
11
+ />
12
+ )
13
+ )
14
+ Card.displayName = 'Card'
15
+
16
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
+ ({ className, ...props }, ref) => (
18
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
19
+ )
20
+ )
21
+ CardHeader.displayName = 'CardHeader'
22
+
23
+ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ className={cn('leading-none font-semibold tracking-tight', className)}
28
+ {...props}
29
+ />
30
+ )
31
+ )
32
+ CardTitle.displayName = 'CardTitle'
33
+
34
+ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
35
+ ({ className, ...props }, ref) => (
36
+ <div ref={ref} className={cn('text-muted-foreground text-sm', className)} {...props} />
37
+ )
38
+ )
39
+ CardDescription.displayName = 'CardDescription'
40
+
41
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
42
+ ({ className, ...props }, ref) => (
43
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
44
+ )
45
+ )
46
+ CardContent.displayName = 'CardContent'
47
+
48
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
49
+ ({ className, ...props }, ref) => (
50
+ <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
51
+ )
52
+ )
53
+ CardFooter.displayName = 'CardFooter'
54
+
55
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
lightrag_webui/src/components/ui/Checkbox.tsx CHANGED
@@ -23,4 +23,4 @@ const Checkbox = React.forwardRef<
23
  ))
24
  Checkbox.displayName = CheckboxPrimitive.Root.displayName
25
 
26
- export { Checkbox }
 
23
  ))
24
  Checkbox.displayName = CheckboxPrimitive.Root.displayName
25
 
26
+ export default Checkbox
lightrag_webui/src/components/ui/DataTable.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
2
+
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow
10
+ } from '@/components/ui/Table'
11
+
12
+ interface DataTableProps<TData, TValue> {
13
+ columns: ColumnDef<TData, TValue>[]
14
+ data: TData[]
15
+ }
16
+
17
+ export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
18
+ const table = useReactTable({
19
+ data,
20
+ columns,
21
+ getCoreRowModel: getCoreRowModel()
22
+ })
23
+
24
+ return (
25
+ <div className="rounded-md border">
26
+ <Table>
27
+ <TableHeader>
28
+ {table.getHeaderGroups().map((headerGroup) => (
29
+ <TableRow key={headerGroup.id}>
30
+ {headerGroup.headers.map((header) => {
31
+ return (
32
+ <TableHead key={header.id}>
33
+ {header.isPlaceholder
34
+ ? null
35
+ : flexRender(header.column.columnDef.header, header.getContext())}
36
+ </TableHead>
37
+ )
38
+ })}
39
+ </TableRow>
40
+ ))}
41
+ </TableHeader>
42
+ <TableBody>
43
+ {table.getRowModel().rows?.length ? (
44
+ table.getRowModel().rows.map((row) => (
45
+ <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
46
+ {row.getVisibleCells().map((cell) => (
47
+ <TableCell key={cell.id}>
48
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
49
+ </TableCell>
50
+ ))}
51
+ </TableRow>
52
+ ))
53
+ ) : (
54
+ <TableRow>
55
+ <TableCell colSpan={columns.length} className="h-24 text-center">
56
+ No results.
57
+ </TableCell>
58
+ </TableRow>
59
+ )}
60
+ </TableBody>
61
+ </Table>
62
+ </div>
63
+ )
64
+ }
lightrag_webui/src/components/ui/Dialog.tsx CHANGED
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
36
  <DialogPrimitive.Content
37
  ref={ref}
38
  className={cn(
39
- 'bg-background 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%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
40
  className
41
  )}
42
  {...props}
@@ -65,7 +65,7 @@ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
65
  DialogFooter.displayName = 'DialogFooter'
66
 
67
  const DialogTitle = React.forwardRef<
68
- React.ElementRef<typeof DialogPrimitive.Title>,
69
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
70
  >(({ className, ...props }, ref) => (
71
  <DialogPrimitive.Title
@@ -77,7 +77,7 @@ const DialogTitle = React.forwardRef<
77
  DialogTitle.displayName = DialogPrimitive.Title.displayName
78
 
79
  const DialogDescription = React.forwardRef<
80
- React.ElementRef<typeof DialogPrimitive.Description>,
81
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
82
  >(({ className, ...props }, ref) => (
83
  <DialogPrimitive.Description
 
36
  <DialogPrimitive.Content
37
  ref={ref}
38
  className={cn(
39
+ 'bg-background 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-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
40
  className
41
  )}
42
  {...props}
 
65
  DialogFooter.displayName = 'DialogFooter'
66
 
67
  const DialogTitle = React.forwardRef<
68
+ React.ComponentRef<typeof DialogPrimitive.Title>,
69
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
70
  >(({ className, ...props }, ref) => (
71
  <DialogPrimitive.Title
 
77
  DialogTitle.displayName = DialogPrimitive.Title.displayName
78
 
79
  const DialogDescription = React.forwardRef<
80
+ React.ComponentRef<typeof DialogPrimitive.Description>,
81
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
82
  >(({ className, ...props }, ref) => (
83
  <DialogPrimitive.Description
lightrag_webui/src/components/ui/EmptyCard.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from '@/lib/utils'
2
+ import { Card, CardDescription, CardTitle } from '@/components/ui/Card'
3
+ import { FilesIcon } from 'lucide-react'
4
+
5
+ interface EmptyCardProps extends React.ComponentPropsWithoutRef<typeof Card> {
6
+ title: string
7
+ description?: string
8
+ action?: React.ReactNode
9
+ icon?: React.ComponentType<{ className?: string }>
10
+ }
11
+
12
+ export default function EmptyCard({
13
+ title,
14
+ description,
15
+ icon: Icon = FilesIcon,
16
+ action,
17
+ className,
18
+ ...props
19
+ }: EmptyCardProps) {
20
+ return (
21
+ <Card
22
+ className={cn(
23
+ 'flex w-full flex-col items-center justify-center space-y-6 bg-transparent p-16',
24
+ className
25
+ )}
26
+ {...props}
27
+ >
28
+ <div className="mr-4 shrink-0 rounded-full border border-dashed p-4">
29
+ <Icon className="text-muted-foreground size-8" aria-hidden="true" />
30
+ </div>
31
+ <div className="flex flex-col items-center gap-1.5 text-center">
32
+ <CardTitle>{title}</CardTitle>
33
+ {description ? <CardDescription>{description}</CardDescription> : null}
34
+ </div>
35
+ {action ? action : null}
36
+ </Card>
37
+ )
38
+ }
lightrag_webui/src/components/ui/FileUploader.tsx ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @see https://github.com/sadmann7/file-uploader
3
+ */
4
+
5
+ import * as React from 'react'
6
+ import { FileText, Upload, X } from 'lucide-react'
7
+ import Dropzone, { type DropzoneProps, type FileRejection } from 'react-dropzone'
8
+ import { toast } from 'sonner'
9
+
10
+ import { cn } from '@/lib/utils'
11
+ import { useControllableState } from '@radix-ui/react-use-controllable-state'
12
+ import Button from '@/components/ui/Button'
13
+ import Progress from '@/components/ui/Progress'
14
+ import { ScrollArea } from '@/components/ui/ScrollArea'
15
+ import { supportedFileTypes } from '@/lib/constants'
16
+
17
+ interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
18
+ /**
19
+ * Value of the uploader.
20
+ * @type File[]
21
+ * @default undefined
22
+ * @example value={files}
23
+ */
24
+ value?: File[]
25
+
26
+ /**
27
+ * Function to be called when the value changes.
28
+ * @type (files: File[]) => void
29
+ * @default undefined
30
+ * @example onValueChange={(files) => setFiles(files)}
31
+ */
32
+ onValueChange?: (files: File[]) => void
33
+
34
+ /**
35
+ * Function to be called when files are uploaded.
36
+ * @type (files: File[]) => Promise<void>
37
+ * @default undefined
38
+ * @example onUpload={(files) => uploadFiles(files)}
39
+ */
40
+ onUpload?: (files: File[]) => Promise<void>
41
+
42
+ /**
43
+ * Progress of the uploaded files.
44
+ * @type Record<string, number> | undefined
45
+ * @default undefined
46
+ * @example progresses={{ "file1.png": 50 }}
47
+ */
48
+ progresses?: Record<string, number>
49
+
50
+ /**
51
+ * Accepted file types for the uploader.
52
+ * @type { [key: string]: string[]}
53
+ * @default
54
+ * ```ts
55
+ * { "text/*": [] }
56
+ * ```
57
+ * @example accept={["text/plain", "application/pdf"]}
58
+ */
59
+ accept?: DropzoneProps['accept']
60
+
61
+ /**
62
+ * Maximum file size for the uploader.
63
+ * @type number | undefined
64
+ * @default 1024 * 1024 * 200 // 200MB
65
+ * @example maxSize={1024 * 1024 * 2} // 2MB
66
+ */
67
+ maxSize?: DropzoneProps['maxSize']
68
+
69
+ /**
70
+ * Maximum number of files for the uploader.
71
+ * @type number | undefined
72
+ * @default 1
73
+ * @example maxFileCount={4}
74
+ */
75
+ maxFileCount?: DropzoneProps['maxFiles']
76
+
77
+ /**
78
+ * Whether the uploader should accept multiple files.
79
+ * @type boolean
80
+ * @default false
81
+ * @example multiple
82
+ */
83
+ multiple?: boolean
84
+
85
+ /**
86
+ * Whether the uploader is disabled.
87
+ * @type boolean
88
+ * @default false
89
+ * @example disabled
90
+ */
91
+ disabled?: boolean
92
+
93
+ description?: string
94
+ }
95
+
96
+ function formatBytes(
97
+ bytes: number,
98
+ opts: {
99
+ decimals?: number
100
+ sizeType?: 'accurate' | 'normal'
101
+ } = {}
102
+ ) {
103
+ const { decimals = 0, sizeType = 'normal' } = opts
104
+
105
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
106
+ const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB']
107
+ if (bytes === 0) return '0 Byte'
108
+ const i = Math.floor(Math.log(bytes) / Math.log(1024))
109
+ return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${
110
+ sizeType === 'accurate' ? (accurateSizes[i] ?? 'Bytes') : (sizes[i] ?? 'Bytes')
111
+ }`
112
+ }
113
+
114
+ function FileUploader(props: FileUploaderProps) {
115
+ const {
116
+ value: valueProp,
117
+ onValueChange,
118
+ onUpload,
119
+ progresses,
120
+ accept = supportedFileTypes,
121
+ maxSize = 1024 * 1024 * 200,
122
+ maxFileCount = 1,
123
+ multiple = false,
124
+ disabled = false,
125
+ description,
126
+ className,
127
+ ...dropzoneProps
128
+ } = props
129
+
130
+ const [files, setFiles] = useControllableState({
131
+ prop: valueProp,
132
+ onChange: onValueChange
133
+ })
134
+
135
+ const onDrop = React.useCallback(
136
+ (acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
137
+ if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
138
+ toast.error('Cannot upload more than 1 file at a time')
139
+ return
140
+ }
141
+
142
+ if ((files?.length ?? 0) + acceptedFiles.length > maxFileCount) {
143
+ toast.error(`Cannot upload more than ${maxFileCount} files`)
144
+ return
145
+ }
146
+
147
+ const newFiles = acceptedFiles.map((file) =>
148
+ Object.assign(file, {
149
+ preview: URL.createObjectURL(file)
150
+ })
151
+ )
152
+
153
+ const updatedFiles = files ? [...files, ...newFiles] : newFiles
154
+
155
+ setFiles(updatedFiles)
156
+
157
+ if (rejectedFiles.length > 0) {
158
+ rejectedFiles.forEach(({ file }) => {
159
+ toast.error(`File ${file.name} was rejected`)
160
+ })
161
+ }
162
+
163
+ if (onUpload && updatedFiles.length > 0 && updatedFiles.length <= maxFileCount) {
164
+ const target = updatedFiles.length > 0 ? `${updatedFiles.length} files` : 'file'
165
+
166
+ toast.promise(onUpload(updatedFiles), {
167
+ loading: `Uploading ${target}...`,
168
+ success: () => {
169
+ setFiles([])
170
+ return `${target} uploaded`
171
+ },
172
+ error: `Failed to upload ${target}`
173
+ })
174
+ }
175
+ },
176
+
177
+ [files, maxFileCount, multiple, onUpload, setFiles]
178
+ )
179
+
180
+ function onRemove(index: number) {
181
+ if (!files) return
182
+ const newFiles = files.filter((_, i) => i !== index)
183
+ setFiles(newFiles)
184
+ onValueChange?.(newFiles)
185
+ }
186
+
187
+ // Revoke preview url when component unmounts
188
+ React.useEffect(() => {
189
+ return () => {
190
+ if (!files) return
191
+ files.forEach((file) => {
192
+ if (isFileWithPreview(file)) {
193
+ URL.revokeObjectURL(file.preview)
194
+ }
195
+ })
196
+ }
197
+ // eslint-disable-next-line react-hooks/exhaustive-deps
198
+ }, [])
199
+
200
+ const isDisabled = disabled || (files?.length ?? 0) >= maxFileCount
201
+
202
+ return (
203
+ <div className="relative flex flex-col gap-6 overflow-hidden">
204
+ <Dropzone
205
+ onDrop={onDrop}
206
+ accept={accept}
207
+ maxSize={maxSize}
208
+ maxFiles={maxFileCount}
209
+ multiple={maxFileCount > 1 || multiple}
210
+ disabled={isDisabled}
211
+ >
212
+ {({ getRootProps, getInputProps, isDragActive }) => (
213
+ <div
214
+ {...getRootProps()}
215
+ className={cn(
216
+ 'group border-muted-foreground/25 hover:bg-muted/25 relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed px-5 py-2.5 text-center transition',
217
+ 'ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
218
+ isDragActive && 'border-muted-foreground/50',
219
+ isDisabled && 'pointer-events-none opacity-60',
220
+ className
221
+ )}
222
+ {...dropzoneProps}
223
+ >
224
+ <input {...getInputProps()} />
225
+ {isDragActive ? (
226
+ <div className="flex flex-col items-center justify-center gap-4 sm:px-5">
227
+ <div className="rounded-full border border-dashed p-3">
228
+ <Upload className="text-muted-foreground size-7" aria-hidden="true" />
229
+ </div>
230
+ <p className="text-muted-foreground font-medium">Drop the files here</p>
231
+ </div>
232
+ ) : (
233
+ <div className="flex flex-col items-center justify-center gap-4 sm:px-5">
234
+ <div className="rounded-full border border-dashed p-3">
235
+ <Upload className="text-muted-foreground size-7" aria-hidden="true" />
236
+ </div>
237
+ <div className="flex flex-col gap-px">
238
+ <p className="text-muted-foreground font-medium">
239
+ Drag and drop files here, or click to select files
240
+ </p>
241
+ {description ? (
242
+ <p className="text-muted-foreground/70 text-sm">{description}</p>
243
+ ) : (
244
+ <p className="text-muted-foreground/70 text-sm">
245
+ You can upload
246
+ {maxFileCount > 1
247
+ ? ` ${maxFileCount === Infinity ? 'multiple' : maxFileCount}
248
+ files (up to ${formatBytes(maxSize)} each)`
249
+ : ` a file with ${formatBytes(maxSize)}`}
250
+ Supported formats: TXT, MD, DOC, PDF, PPTX
251
+ </p>
252
+ )}
253
+ </div>
254
+ </div>
255
+ )}
256
+ </div>
257
+ )}
258
+ </Dropzone>
259
+ {files?.length ? (
260
+ <ScrollArea className="h-fit w-full px-3">
261
+ <div className="flex max-h-48 flex-col gap-4">
262
+ {files?.map((file, index) => (
263
+ <FileCard
264
+ key={index}
265
+ file={file}
266
+ onRemove={() => onRemove(index)}
267
+ progress={progresses?.[file.name]}
268
+ />
269
+ ))}
270
+ </div>
271
+ </ScrollArea>
272
+ ) : null}
273
+ </div>
274
+ )
275
+ }
276
+
277
+ interface FileCardProps {
278
+ file: File
279
+ onRemove: () => void
280
+ progress?: number
281
+ }
282
+
283
+ function FileCard({ file, progress, onRemove }: FileCardProps) {
284
+ return (
285
+ <div className="relative flex items-center gap-2.5">
286
+ <div className="flex flex-1 gap-2.5">
287
+ {isFileWithPreview(file) ? <FilePreview file={file} /> : null}
288
+ <div className="flex w-full flex-col gap-2">
289
+ <div className="flex flex-col gap-px">
290
+ <p className="text-foreground/80 line-clamp-1 text-sm font-medium">{file.name}</p>
291
+ <p className="text-muted-foreground text-xs">{formatBytes(file.size)}</p>
292
+ </div>
293
+ {progress ? <Progress value={progress} /> : null}
294
+ </div>
295
+ </div>
296
+ <div className="flex items-center gap-2">
297
+ <Button type="button" variant="outline" size="icon" className="size-7" onClick={onRemove}>
298
+ <X className="size-4" aria-hidden="true" />
299
+ <span className="sr-only">Remove file</span>
300
+ </Button>
301
+ </div>
302
+ </div>
303
+ )
304
+ }
305
+
306
+ function isFileWithPreview(file: File): file is File & { preview: string } {
307
+ return 'preview' in file && typeof file.preview === 'string'
308
+ }
309
+
310
+ interface FilePreviewProps {
311
+ file: File & { preview: string }
312
+ }
313
+
314
+ function FilePreview({ file }: FilePreviewProps) {
315
+ if (file.type.startsWith('image/')) {
316
+ return <div className="aspect-square shrink-0 rounded-md object-cover" />
317
+ }
318
+
319
+ return <FileText className="text-muted-foreground size-10" aria-hidden="true" />
320
+ }
321
+
322
+ export default FileUploader
lightrag_webui/src/components/ui/NumberInput.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChevronDown, ChevronUp } from 'lucide-react'
2
+ import { forwardRef, useCallback, useEffect, useState } from 'react'
3
+ import { NumericFormat, NumericFormatProps } from 'react-number-format'
4
+ import Button from '@/components/ui/Button'
5
+ import Input from '@/components/ui/Input'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ export interface NumberInputProps extends Omit<NumericFormatProps, 'value' | 'onValueChange'> {
9
+ stepper?: number
10
+ thousandSeparator?: string
11
+ placeholder?: string
12
+ defaultValue?: number
13
+ min?: number
14
+ max?: number
15
+ value?: number // Controlled value
16
+ suffix?: string
17
+ prefix?: string
18
+ onValueChange?: (value: number | undefined) => void
19
+ fixedDecimalScale?: boolean
20
+ decimalScale?: number
21
+ }
22
+
23
+ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
24
+ (
25
+ {
26
+ stepper,
27
+ thousandSeparator,
28
+ placeholder,
29
+ defaultValue,
30
+ min = -Infinity,
31
+ max = Infinity,
32
+ onValueChange,
33
+ fixedDecimalScale = false,
34
+ decimalScale = 0,
35
+ className = undefined,
36
+ suffix,
37
+ prefix,
38
+ value: controlledValue,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ const [value, setValue] = useState<number | undefined>(controlledValue ?? defaultValue)
44
+
45
+ const handleIncrement = useCallback(() => {
46
+ setValue((prev) =>
47
+ prev === undefined ? (stepper ?? 1) : Math.min(prev + (stepper ?? 1), max)
48
+ )
49
+ }, [stepper, max])
50
+
51
+ const handleDecrement = useCallback(() => {
52
+ setValue((prev) =>
53
+ prev === undefined ? -(stepper ?? 1) : Math.max(prev - (stepper ?? 1), min)
54
+ )
55
+ }, [stepper, min])
56
+
57
+ useEffect(() => {
58
+ if (controlledValue !== undefined) {
59
+ setValue(controlledValue)
60
+ }
61
+ }, [controlledValue])
62
+
63
+ const handleChange = (values: { value: string; floatValue: number | undefined }) => {
64
+ const newValue = values.floatValue === undefined ? undefined : values.floatValue
65
+ setValue(newValue)
66
+ if (onValueChange) {
67
+ onValueChange(newValue)
68
+ }
69
+ }
70
+
71
+ const handleBlur = () => {
72
+ if (value !== undefined) {
73
+ if (value < min) {
74
+ setValue(min)
75
+ ;(ref as React.RefObject<HTMLInputElement>).current!.value = String(min)
76
+ } else if (value > max) {
77
+ setValue(max)
78
+ ;(ref as React.RefObject<HTMLInputElement>).current!.value = String(max)
79
+ }
80
+ }
81
+ }
82
+
83
+ return (
84
+ <div className="relative flex">
85
+ <NumericFormat
86
+ value={value}
87
+ onValueChange={handleChange}
88
+ thousandSeparator={thousandSeparator}
89
+ decimalScale={decimalScale}
90
+ fixedDecimalScale={fixedDecimalScale}
91
+ allowNegative={min < 0}
92
+ valueIsNumericString
93
+ onBlur={handleBlur}
94
+ max={max}
95
+ min={min}
96
+ suffix={suffix}
97
+ prefix={prefix}
98
+ customInput={(props) => <Input {...props} className={cn('w-full', className)} />}
99
+ placeholder={placeholder}
100
+ className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
101
+ getInputRef={ref}
102
+ {...props}
103
+ />
104
+ <div className="absolute top-0 right-0 bottom-0 flex flex-col">
105
+ <Button
106
+ aria-label="Increase value"
107
+ className="border-input h-1/2 rounded-l-none rounded-br-none border-b border-l px-2 focus-visible:relative"
108
+ variant="outline"
109
+ onClick={handleIncrement}
110
+ disabled={value === max}
111
+ >
112
+ <ChevronUp size={15} />
113
+ </Button>
114
+ <Button
115
+ aria-label="Decrease value"
116
+ className="border-input h-1/2 rounded-l-none rounded-tr-none border-b border-l px-2 focus-visible:relative"
117
+ variant="outline"
118
+ onClick={handleDecrement}
119
+ disabled={value === min}
120
+ >
121
+ <ChevronDown size={15} />
122
+ </Button>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+ )
128
+
129
+ NumberInput.displayName = 'NumberInput'
130
+
131
+ export default NumberInput
lightrag_webui/src/components/ui/Progress.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as ProgressPrimitive from '@radix-ui/react-progress'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const Progress = React.forwardRef<
7
+ React.ComponentRef<typeof ProgressPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
9
+ >(({ className, value, ...props }, ref) => (
10
+ <ProgressPrimitive.Root
11
+ ref={ref}
12
+ className={cn('bg-secondary relative h-4 w-full overflow-hidden rounded-full', className)}
13
+ {...props}
14
+ >
15
+ <ProgressPrimitive.Indicator
16
+ className="bg-primary h-full w-full flex-1 transition-all"
17
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
18
+ />
19
+ </ProgressPrimitive.Root>
20
+ ))
21
+ Progress.displayName = ProgressPrimitive.Root.displayName
22
+
23
+ export default Progress
lightrag_webui/src/components/ui/ScrollArea.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
3
+
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const ScrollArea = React.forwardRef<
7
+ React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
9
+ >(({ className, children, ...props }, ref) => (
10
+ <ScrollAreaPrimitive.Root
11
+ ref={ref}
12
+ className={cn('relative overflow-hidden', className)}
13
+ {...props}
14
+ >
15
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
16
+ {children}
17
+ </ScrollAreaPrimitive.Viewport>
18
+ <ScrollBar />
19
+ <ScrollAreaPrimitive.Corner />
20
+ </ScrollAreaPrimitive.Root>
21
+ ))
22
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23
+
24
+ const ScrollBar = React.forwardRef<
25
+ React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
26
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
27
+ >(({ className, orientation = 'vertical', ...props }, ref) => (
28
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
29
+ ref={ref}
30
+ orientation={orientation}
31
+ className={cn(
32
+ 'flex touch-none transition-colors select-none',
33
+ orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
34
+ orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
35
+ className
36
+ )}
37
+ {...props}
38
+ >
39
+ <ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" />
40
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
41
+ ))
42
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
43
+
44
+ export { ScrollArea, ScrollBar }
lightrag_webui/src/components/ui/Select.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as SelectPrimitive from '@radix-ui/react-select'
3
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ const Select = SelectPrimitive.Root
8
+
9
+ const SelectGroup = SelectPrimitive.Group
10
+
11
+ const SelectValue = SelectPrimitive.Value
12
+
13
+ const SelectTrigger = React.forwardRef<
14
+ React.ComponentRef<typeof SelectPrimitive.Trigger>,
15
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
16
+ >(({ className, children, ...props }, ref) => (
17
+ <SelectPrimitive.Trigger
18
+ ref={ref}
19
+ className={cn(
20
+ 'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ {children}
26
+ <SelectPrimitive.Icon asChild>
27
+ <ChevronDown className="h-4 w-4 opacity-50" />
28
+ </SelectPrimitive.Icon>
29
+ </SelectPrimitive.Trigger>
30
+ ))
31
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
32
+
33
+ const SelectScrollUpButton = React.forwardRef<
34
+ React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
35
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
36
+ >(({ className, ...props }, ref) => (
37
+ <SelectPrimitive.ScrollUpButton
38
+ ref={ref}
39
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
40
+ {...props}
41
+ >
42
+ <ChevronUp className="h-4 w-4" />
43
+ </SelectPrimitive.ScrollUpButton>
44
+ ))
45
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
46
+
47
+ const SelectScrollDownButton = React.forwardRef<
48
+ React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
49
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
50
+ >(({ className, ...props }, ref) => (
51
+ <SelectPrimitive.ScrollDownButton
52
+ ref={ref}
53
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
54
+ {...props}
55
+ >
56
+ <ChevronDown className="h-4 w-4" />
57
+ </SelectPrimitive.ScrollDownButton>
58
+ ))
59
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
60
+
61
+ const SelectContent = React.forwardRef<
62
+ React.ComponentRef<typeof SelectPrimitive.Content>,
63
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
64
+ >(({ className, children, position = 'popper', ...props }, ref) => (
65
+ <SelectPrimitive.Portal>
66
+ <SelectPrimitive.Content
67
+ ref={ref}
68
+ className={cn(
69
+ 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md',
70
+ position === 'popper' &&
71
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
72
+ className
73
+ )}
74
+ position={position}
75
+ {...props}
76
+ >
77
+ <SelectScrollUpButton />
78
+ <SelectPrimitive.Viewport
79
+ className={cn(
80
+ 'p-1',
81
+ position === 'popper' &&
82
+ 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
83
+ )}
84
+ >
85
+ {children}
86
+ </SelectPrimitive.Viewport>
87
+ <SelectScrollDownButton />
88
+ </SelectPrimitive.Content>
89
+ </SelectPrimitive.Portal>
90
+ ))
91
+ SelectContent.displayName = SelectPrimitive.Content.displayName
92
+
93
+ const SelectLabel = React.forwardRef<
94
+ React.ComponentRef<typeof SelectPrimitive.Label>,
95
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
96
+ >(({ className, ...props }, ref) => (
97
+ <SelectPrimitive.Label
98
+ ref={ref}
99
+ className={cn('py-1.5 pr-2 pl-8 text-sm font-semibold', className)}
100
+ {...props}
101
+ />
102
+ ))
103
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
104
+
105
+ const SelectItem = React.forwardRef<
106
+ React.ComponentRef<typeof SelectPrimitive.Item>,
107
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
108
+ >(({ className, children, ...props }, ref) => (
109
+ <SelectPrimitive.Item
110
+ ref={ref}
111
+ className={cn(
112
+ 'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
113
+ className
114
+ )}
115
+ {...props}
116
+ >
117
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
118
+ <SelectPrimitive.ItemIndicator>
119
+ <Check className="h-4 w-4" />
120
+ </SelectPrimitive.ItemIndicator>
121
+ </span>
122
+
123
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
124
+ </SelectPrimitive.Item>
125
+ ))
126
+ SelectItem.displayName = SelectPrimitive.Item.displayName
127
+
128
+ const SelectSeparator = React.forwardRef<
129
+ React.ComponentRef<typeof SelectPrimitive.Separator>,
130
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
131
+ >(({ className, ...props }, ref) => (
132
+ <SelectPrimitive.Separator
133
+ ref={ref}
134
+ className={cn('bg-muted -mx-1 my-1 h-px', className)}
135
+ {...props}
136
+ />
137
+ ))
138
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
139
+
140
+ export {
141
+ Select,
142
+ SelectGroup,
143
+ SelectValue,
144
+ SelectTrigger,
145
+ SelectContent,
146
+ SelectLabel,
147
+ SelectItem,
148
+ SelectSeparator,
149
+ SelectScrollUpButton,
150
+ SelectScrollDownButton
151
+ }