diff --git a/lightrag/api/lightrag_server.py b/lightrag/api/lightrag_server.py index a392e67a8c2bae99d25135f475cfb819601dbfc3..751664f626e5463f8162f1956a2c64856c6a83ab 100644 --- a/lightrag/api/lightrag_server.py +++ b/lightrag/api/lightrag_server.py @@ -19,6 +19,7 @@ from lightrag import LightRAG, QueryParam from lightrag.types import GPTKeywordExtractionFormat from lightrag.api import __api_version__ from lightrag.utils import EmbeddingFunc +from lightrag.base import DocStatus, DocProcessingStatus from enum import Enum from pathlib import Path import shutil @@ -253,10 +254,8 @@ def display_splash_screen(args: argparse.Namespace) -> None: ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/docs") ASCIIColors.white(" ├─ Alternative Documentation (local): ", end="") ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/redoc") - ASCIIColors.white(" ├─ WebUI (local): ", end="") + ASCIIColors.white(" └─ WebUI (local): ", end="") ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/webui") - ASCIIColors.white(" └─ Graph Viewer (local): ", end="") - ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/graph-viewer") ASCIIColors.yellow("\n📝 Note:") ASCIIColors.white(""" Since the server is running on 0.0.0.0: @@ -693,6 +692,22 @@ class InsertResponse(BaseModel): message: str +class DocStatusResponse(BaseModel): + id: str + content_summary: str + content_length: int + status: DocStatus + created_at: str + updated_at: str + chunks_count: Optional[int] = None + error: Optional[str] = None + metadata: Optional[dict[str, Any]] = None + + +class DocsStatusesResponse(BaseModel): + statuses: Dict[DocStatus, List[DocStatusResponse]] = {} + + def QueryRequestToQueryParams(request: QueryRequest): param = QueryParam(mode=request.mode, stream=request.stream) if request.only_need_context is not None: @@ -1728,20 +1743,57 @@ def create_app(args): app.include_router(ollama_api.router, prefix="/api") @app.get("/documents", dependencies=[Depends(optional_api_key)]) - async def documents(): - """Get current system status""" - return doc_manager.indexed_files + async def documents() -> DocsStatusesResponse: + """ + Get documents statuses + Returns: + DocsStatusesResponse: A response object containing a dictionary where keys are DocStatus + and values are lists of DocStatusResponse objects representing documents in each status category. + """ + try: + statuses = ( + DocStatus.PENDING, + DocStatus.PROCESSING, + DocStatus.PROCESSED, + DocStatus.FAILED, + ) + + tasks = [rag.get_docs_by_status(status) for status in statuses] + results: List[Dict[str, DocProcessingStatus]] = await asyncio.gather(*tasks) + + response = DocsStatusesResponse() + + for idx, result in enumerate(results): + status = statuses[idx] + for doc_id, doc_status in result.items(): + if status not in response.statuses: + response.statuses[status] = [] + response.statuses[status].append( + DocStatusResponse( + id=doc_id, + content_summary=doc_status.content_summary, + content_length=doc_status.content_length, + status=doc_status.status, + created_at=doc_status.created_at, + updated_at=doc_status.updated_at, + chunks_count=doc_status.chunks_count, + error=doc_status.error, + metadata=doc_status.metadata, + ) + ) + return response + except Exception as e: + logging.error(f"Error GET /documents: {str(e)}") + logging.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) @app.get("/health", dependencies=[Depends(optional_api_key)]) async def get_status(): """Get current system status""" - files = doc_manager.scan_directory() return { "status": "healthy", "working_directory": str(args.working_dir), "input_directory": str(args.input_dir), - "indexed_files": [str(f) for f in files], - "indexed_files_count": len(files), "configuration": { # LLM configuration binding/host address (if applicable)/model (if applicable) "llm_binding": args.llm_binding, @@ -1760,17 +1812,9 @@ def create_app(args): } # Webui mount webui/index.html - webui_dir = Path(__file__).parent / "webui" - app.mount( - "/graph-viewer", - StaticFiles(directory=webui_dir, html=True), - name="webui", - ) - - # Serve the static files - static_dir = Path(__file__).parent / "static" + static_dir = Path(__file__).parent / "webui" static_dir.mkdir(exist_ok=True) - app.mount("/webui", StaticFiles(directory=static_dir, html=True), name="static") + app.mount("/webui", StaticFiles(directory=static_dir, html=True), name="webui") return app diff --git a/lightrag/api/static/README.md b/lightrag/api/static/README.md deleted file mode 100644 index a8c6b1f39120dd26ab5a6ed63b3ade8c774e59e8..0000000000000000000000000000000000000000 --- a/lightrag/api/static/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# LightRag Webui -A simple webui to interact with the lightrag datalake diff --git a/lightrag/api/static/favicon.ico b/lightrag/api/static/favicon.ico deleted file mode 100644 index 4bace9a6755077ba8551774e9161351349aee6c2..0000000000000000000000000000000000000000 --- a/lightrag/api/static/favicon.ico +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:26d6dfa1f5357416c10b39969c6e22843f58c518928bc59e828660ba5746ef94 -size 751385 diff --git a/lightrag/api/static/index.html b/lightrag/api/static/index.html deleted file mode 100644 index 75b26c5e3009e0da5ffecd004ed3ce4903ebb785..0000000000000000000000000000000000000000 --- a/lightrag/api/static/index.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - LightRAG Interface - - - - - -
- -
- -
- - -
-
-
- - - -
- - - - - diff --git a/lightrag/api/static/js/api.js b/lightrag/api/static/js/api.js deleted file mode 100644 index b610eb10a675f8a8821eb55b2effa22adf04f5c8..0000000000000000000000000000000000000000 --- a/lightrag/api/static/js/api.js +++ /dev/null @@ -1,408 +0,0 @@ -// State management -const state = { - apiKey: localStorage.getItem('apiKey') || '', - files: [], - indexedFiles: [], - currentPage: 'file-manager' -}; - -// Utility functions -const showToast = (message, duration = 3000) => { - const toast = document.getElementById('toast'); - toast.querySelector('div').textContent = message; - toast.classList.remove('hidden'); - setTimeout(() => toast.classList.add('hidden'), duration); -}; - -const fetchWithAuth = async (url, options = {}) => { - const headers = { - ...(options.headers || {}), - ...(state.apiKey ? { 'X-API-Key': state.apiKey } : {}) // Use X-API-Key instead of Bearer - }; - return fetch(url, { ...options, headers }); -}; - - -// Page renderers -const pages = { - 'file-manager': () => ` -
-

File Manager

- -
- - -
- -
-

Selected Files

-
-
- -
- - - -
- -
-

Indexed Files

-
-
- - -
- `, - - 'query': () => ` -
-

Query Database

- -
-
- - -
- -
- - -
- - - -
-
-
- `, - - 'knowledge-graph': () => ` -
-
- - - -

Under Construction

-

Knowledge graph visualization will be available in a future update.

-
-
- `, - - 'status': () => ` -
-

System Status

-
-
-

System Health

-
-
-
-

Configuration

-
-
-
-
- `, - - 'settings': () => ` -
-

Settings

- -
-
-
- - -
- - -
-
-
- ` -}; - -// Page handlers -const handlers = { - 'file-manager': () => { - const fileInput = document.getElementById('fileInput'); - const dropZone = fileInput.parentElement.parentElement; - const fileList = document.querySelector('#fileList div'); - const indexedFiles = document.querySelector('#indexedFiles div'); - const uploadBtn = document.getElementById('uploadBtn'); - - const updateFileList = () => { - fileList.innerHTML = state.files.map(file => ` -
- ${file.name} - -
- `).join(''); - }; - - const updateIndexedFiles = async () => { - const response = await fetchWithAuth('/health'); - const data = await response.json(); - indexedFiles.innerHTML = data.indexed_files.map(file => ` -
- ${file} -
- `).join(''); - }; - - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('border-blue-500'); - }); - - dropZone.addEventListener('dragleave', () => { - dropZone.classList.remove('border-blue-500'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('border-blue-500'); - const files = Array.from(e.dataTransfer.files); - state.files.push(...files); - updateFileList(); - }); - - fileInput.addEventListener('change', () => { - state.files.push(...Array.from(fileInput.files)); - updateFileList(); - }); - - uploadBtn.addEventListener('click', async () => { - if (state.files.length === 0) { - showToast('Please select files to upload'); - return; - } - let apiKey = localStorage.getItem('apiKey') || ''; - const progress = document.getElementById('uploadProgress'); - const progressBar = progress.querySelector('div'); - const statusText = document.getElementById('uploadStatus'); - progress.classList.remove('hidden'); - - for (let i = 0; i < state.files.length; i++) { - const formData = new FormData(); - formData.append('file', state.files[i]); - - try { - await fetch('/documents/upload', { - method: 'POST', - headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {}, - body: formData - }); - - const percentage = ((i + 1) / state.files.length) * 100; - progressBar.style.width = `${percentage}%`; - statusText.textContent = `${i + 1}/${state.files.length}`; - } catch (error) { - console.error('Upload error:', error); - } - } - progress.classList.add('hidden'); - }); - - rescanBtn.addEventListener('click', async () => { - const progress = document.getElementById('uploadProgress'); - const progressBar = progress.querySelector('div'); - const statusText = document.getElementById('uploadStatus'); - progress.classList.remove('hidden'); - - try { - // Start the scanning process - const scanResponse = await fetch('/documents/scan', { - method: 'POST', - }); - - if (!scanResponse.ok) { - throw new Error('Scan failed to start'); - } - - // Start polling for progress - const pollInterval = setInterval(async () => { - const progressResponse = await fetch('/documents/scan-progress'); - const progressData = await progressResponse.json(); - - // Update progress bar - progressBar.style.width = `${progressData.progress}%`; - - // Update status text - if (progressData.total_files > 0) { - statusText.textContent = `Processing ${progressData.current_file} (${progressData.indexed_count}/${progressData.total_files})`; - } - - // Check if scanning is complete - if (!progressData.is_scanning) { - clearInterval(pollInterval); - progress.classList.add('hidden'); - statusText.textContent = 'Scan complete!'; - } - }, 1000); // Poll every second - - } catch (error) { - console.error('Upload error:', error); - progress.classList.add('hidden'); - statusText.textContent = 'Error during scanning process'; - } - }); - - - updateIndexedFiles(); - }, - - 'query': () => { - const queryBtn = document.getElementById('queryBtn'); - const queryInput = document.getElementById('queryInput'); - const queryMode = document.getElementById('queryMode'); - const queryResult = document.getElementById('queryResult'); - - let apiKey = localStorage.getItem('apiKey') || ''; - - queryBtn.addEventListener('click', async () => { - const query = queryInput.value.trim(); - if (!query) { - showToast('Please enter a query'); - return; - } - - queryBtn.disabled = true; - queryBtn.innerHTML = ` - - - - - Processing... - `; - - try { - const response = await fetchWithAuth('/query', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - query, - mode: queryMode.value, - stream: false, - only_need_context: false - }) - }); - - const data = await response.json(); - queryResult.innerHTML = marked.parse(data.response); - } catch (error) { - showToast('Error processing query'); - } finally { - queryBtn.disabled = false; - queryBtn.textContent = 'Send Query'; - } - }); - }, - - 'status': async () => { - const healthStatus = document.getElementById('healthStatus'); - const configStatus = document.getElementById('configStatus'); - - try { - const response = await fetchWithAuth('/health'); - const data = await response.json(); - - healthStatus.innerHTML = ` -
-
-
- ${data.status} -
-
-

Working Directory: ${data.working_directory}

-

Input Directory: ${data.input_directory}

-

Indexed Files: ${data.indexed_files_count}

-
-
- `; - - configStatus.innerHTML = Object.entries(data.configuration) - .map(([key, value]) => ` -
- ${key}: - ${value} -
- `).join(''); - } catch (error) { - showToast('Error fetching status'); - } - }, - - 'settings': () => { - const saveBtn = document.getElementById('saveSettings'); - const apiKeyInput = document.getElementById('apiKeyInput'); - - saveBtn.addEventListener('click', () => { - state.apiKey = apiKeyInput.value; - localStorage.setItem('apiKey', state.apiKey); - showToast('Settings saved successfully'); - }); - } -}; - -// Navigation handling -document.querySelectorAll('.nav-item').forEach(item => { - item.addEventListener('click', (e) => { - e.preventDefault(); - const page = item.dataset.page; - document.getElementById('content').innerHTML = pages[page](); - if (handlers[page]) handlers[page](); - state.currentPage = page; - }); -}); - -// Initialize with file manager -document.getElementById('content').innerHTML = pages['file-manager'](); -handlers['file-manager'](); - -// Global functions -window.removeFile = (fileName) => { - state.files = state.files.filter(file => file.name !== fileName); - document.querySelector('#fileList div').innerHTML = state.files.map(file => ` -
- ${file.name} - -
- `).join(''); -}; diff --git a/lightrag/api/static/js/graph.js b/lightrag/api/static/js/graph.js deleted file mode 100644 index 140a77811f1049773ca86f4810d4fb2bddf1fe27..0000000000000000000000000000000000000000 --- a/lightrag/api/static/js/graph.js +++ /dev/null @@ -1,211 +0,0 @@ -// js/graph.js -function openGraphModal(label) { - const modal = document.getElementById("graph-modal"); - const graphTitle = document.getElementById("graph-title"); - - if (!modal || !graphTitle) { - console.error("Key element not found"); - return; - } - - graphTitle.textContent = `Knowledge Graph - ${label}`; - modal.style.display = "flex"; - - renderGraph(label); -} - -function closeGraphModal() { - const modal = document.getElementById("graph-modal"); - modal.style.display = "none"; - clearGraph(); -} - -function clearGraph() { - const svg = document.getElementById("graph-svg"); - svg.innerHTML = ""; -} - - -async function getGraph(label) { - try { - const response = await fetch(`/graphs?label=${label}`); - const rawData = await response.json(); - console.log({data: JSON.parse(JSON.stringify(rawData))}); - - const nodes = rawData.nodes - - nodes.forEach(node => { - node.id = Date.now().toString(36) + Math.random().toString(36).substring(2); // 使用 crypto.randomUUID() 生成唯一 UUID - }); - - // Strictly verify edge data - const edges = (rawData.edges || []).map(edge => { - const sourceNode = nodes.find(n => n.labels.includes(edge.source)); - const targetNode = nodes.find(n => n.labels.includes(edge.target) - ) - ; - if (!sourceNode || !targetNode) { - console.warn("NOT VALID EDGE:", edge); - return null; - } - return { - source: sourceNode, - target: targetNode, - type: edge.type || "" - }; - }).filter(edge => edge !== null); - - return {nodes, edges}; - } catch (error) { - console.error("Loading graph failed:", error); - return {nodes: [], edges: []}; - } -} - -async function renderGraph(label) { - const data = await getGraph(label); - - - if (!data.nodes || data.nodes.length === 0) { - d3.select("#graph-svg") - .html(`No valid nodes`); - return; - } - - - const svg = d3.select("#graph-svg"); - const width = svg.node().clientWidth; - const height = svg.node().clientHeight; - - svg.selectAll("*").remove(); - - // Create a force oriented diagram layout - const simulation = d3.forceSimulation(data.nodes) - .force("charge", d3.forceManyBody().strength(-300)) - .force("center", d3.forceCenter(width / 2, height / 2)); - - // Add a connection (if there are valid edges) - if (data.edges.length > 0) { - simulation.force("link", - d3.forceLink(data.edges) - .id(d => d.id) - .distance(100) - ); - } - - // Draw nodes - const nodes = svg.selectAll(".node") - .data(data.nodes) - .enter() - .append("circle") - .attr("class", "node") - .attr("r", 10) - .call(d3.drag() - .on("start", dragStarted) - .on("drag", dragged) - .on("end", dragEnded) - ); - - - svg.append("defs") - .append("marker") - .attr("id", "arrow-out") - .attr("viewBox", "0 0 10 10") - .attr("refX", 8) - .attr("refY", 5) - .attr("markerWidth", 6) - .attr("markerHeight", 6) - .attr("orient", "auto") - .append("path") - .attr("d", "M0,0 L10,5 L0,10 Z") - .attr("fill", "#999"); - - // Draw edges (with arrows) - const links = svg.selectAll(".link") - .data(data.edges) - .enter() - .append("line") - .attr("class", "link") - .attr("marker-end", "url(#arrow-out)"); // Always draw arrows on the target side - - // Edge style configuration - links - .attr("stroke", "#999") - .attr("stroke-width", 2) - .attr("stroke-opacity", 0.8); - - // Draw label (with background box) - const labels = svg.selectAll(".label") - .data(data.nodes) - .enter() - .append("text") - .attr("class", "label") - .text(d => d.labels[0] || "") - .attr("text-anchor", "start") - .attr("dy", "0.3em") - .attr("fill", "#333"); - - // Update Location - simulation.on("tick", () => { - links - .attr("x1", d => { - // Calculate the direction vector from the source node to the target node - const dx = d.target.x - d.source.x; - const dy = d.target.y - d.source.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return d.source.x; // 避免除以零 Avoid dividing by zero - // Adjust the starting point coordinates (source node edge) based on radius 10 - return d.source.x + (dx / distance) * 10; - }) - .attr("y1", d => { - const dx = d.target.x - d.source.x; - const dy = d.target.y - d.source.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return d.source.y; - return d.source.y + (dy / distance) * 10; - }) - .attr("x2", d => { - // Adjust the endpoint coordinates (target node edge) based on a radius of 10 - const dx = d.target.x - d.source.x; - const dy = d.target.y - d.source.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return d.target.x; - return d.target.x - (dx / distance) * 10; - }) - .attr("y2", d => { - const dx = d.target.x - d.source.x; - const dy = d.target.y - d.source.y; - const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return d.target.y; - return d.target.y - (dy / distance) * 10; - }); - - // Update the position of nodes and labels (keep unchanged) - nodes - .attr("cx", d => d.x) - .attr("cy", d => d.y); - - labels - .attr("x", d => d.x + 12) - .attr("y", d => d.y + 4); - }); - - // Drag and drop logic - function dragStarted(event, d) { - if (!event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - } - - function dragged(event, d) { - d.fx = event.x; - d.fy = event.y; - simulation.alpha(0.3).restart(); - } - - function dragEnded(event, d) { - if (!event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - } -} diff --git a/lightrag/api/webui/assets/index-BAeLPZpd.css b/lightrag/api/webui/assets/index-BAeLPZpd.css deleted file mode 100644 index eaee883a26458f7aa99a971c91f7a1438daa6dbc..0000000000000000000000000000000000000000 Binary files a/lightrag/api/webui/assets/index-BAeLPZpd.css and /dev/null differ diff --git a/lightrag/api/webui/assets/index-BMB0OroL.js b/lightrag/api/webui/assets/index-BMB0OroL.js new file mode 100644 index 0000000000000000000000000000000000000000..75b01c44a893ae93c40f1db2ddc50897ea19dee4 Binary files /dev/null and b/lightrag/api/webui/assets/index-BMB0OroL.js differ diff --git a/lightrag/api/webui/assets/index-CF-pcoIl.js b/lightrag/api/webui/assets/index-CF-pcoIl.js deleted file mode 100644 index 3baf5f0c6595c5a9ccb2c8c3e00d941d74414cbb..0000000000000000000000000000000000000000 Binary files a/lightrag/api/webui/assets/index-CF-pcoIl.js and /dev/null differ diff --git a/lightrag/api/webui/assets/index-CLgSwrjG.css b/lightrag/api/webui/assets/index-CLgSwrjG.css new file mode 100644 index 0000000000000000000000000000000000000000..0a63c4080678d8041f962c3ba62c07228fc8b734 Binary files /dev/null and b/lightrag/api/webui/assets/index-CLgSwrjG.css differ diff --git a/lightrag/api/webui/index.html b/lightrag/api/webui/index.html index 3189a1b7247e2da8a307daaaf60577a8c79d6b26..dc92bda5a490eda4bb1b84931b9a4874e04fabe3 100644 Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ diff --git a/lightrag/lightrag.py b/lightrag/lightrag.py index 9909b4b7cff6441b141f260cce328a978085c0d1..a7c5920ee1e2351ce312bdc6dc5cd70b7b95f33a 100644 --- a/lightrag/lightrag.py +++ b/lightrag/lightrag.py @@ -1254,6 +1254,16 @@ class LightRAG: """ return await self.doc_status.get_status_counts() + async def get_docs_by_status( + self, status: DocStatus + ) -> dict[str, DocProcessingStatus]: + """Get documents by status + + Returns: + Dict with document id is keys and document status is values + """ + return await self.doc_status.get_docs_by_status(status) + async def adelete_by_doc_id(self, doc_id: str) -> None: """Delete a document and all its related data diff --git a/lightrag_webui/README.md b/lightrag_webui/README.md index 0a0ac3469cdcf936d2120f34259faddb038b4ec1..ac27f880ccf60bfb66016579548b0f7c7c6bdae3 100644 --- a/lightrag_webui/README.md +++ b/lightrag_webui/README.md @@ -21,7 +21,7 @@ LightRAG WebUI is a React-based web interface for interacting with the LightRAG Run the following command to build the project: ```bash - bun run build + bun run build --emptyOutDir ``` This command will bundle the project and output the built files to the `lightrag/api/webui` directory. diff --git a/lightrag_webui/bun.lock b/lightrag_webui/bun.lock index 8f26b118f3179f33296197980623336c56f6625f..570fa3a68cb27951bc15702d14116fb2efb93f27 100644 --- a/lightrag_webui/bun.lock +++ b/lightrag_webui/bun.lock @@ -4,13 +4,19 @@ "": { "name": "lightrag-webui", "dependencies": { - "@faker-js/faker": "^9.4.0", + "@faker-js/faker": "^9.5.0", + "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", + "@radix-ui/react-use-controllable-state": "^1.1.0", "@react-sigma/core": "^5.0.2", "@react-sigma/graph-search": "^5.0.3", "@react-sigma/layout-circlepack": "^5.0.2", @@ -22,6 +28,7 @@ "@react-sigma/minimap": "^5.0.2", "@sigma/edge-curve": "^3.1.0", "@sigma/node-border": "^3.0.0", + "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -31,9 +38,13 @@ "minisearch": "^7.1.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-dropzone": "^14.3.5", + "react-number-format": "^5.4.3", "seedrandom": "^3.0.5", "sigma": "^3.0.1", + "sonner": "^1.7.4", "tailwind-merge": "^3.0.1", + "tailwind-scrollbar": "^4.0.0", "zustand": "^5.0.3", }, "devDependencies": { @@ -41,19 +52,19 @@ "@stylistic/eslint-plugin-js": "^3.1.0", "@tailwindcss/vite": "^4.0.6", "@types/bun": "^1.2.2", - "@types/node": "^22.13.1", + "@types/node": "^22.13.4", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@types/seedrandom": "^3.0.8", "@vitejs/plugin-react-swc": "^3.8.0", - "eslint": "^9.20.0", + "eslint": "^9.20.1", "eslint-config-prettier": "^10.0.1", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.14.0", + "globals": "^15.15.0", "graphology-types": "^0.24.8", - "prettier": "^3.5.0", + "prettier": "^3.5.1", "prettier-plugin-tailwindcss": "^0.6.11", "tailwindcss": "^4.0.6", "tailwindcss-animate": "^1.0.7", @@ -172,7 +183,7 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], - "@faker-js/faker": ["@faker-js/faker@9.4.0", "", {}, "sha512-85+k0AxaZSTowL0gXp8zYWDIrWclTbRPg/pm/V0dSFZ6W6D4lhcG3uuZl4zLsEKfEvs69xDbLN2cHQudwp95JA=="], + "@faker-js/faker": ["@faker-js/faker@9.5.0", "", {}, "sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw=="], "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="], @@ -206,18 +217,26 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.0", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.6", "", { "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=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "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=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "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=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "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=="], + "@radix-ui/react-compose-refs": ["@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-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="], "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.6", "", { "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=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "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=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "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 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "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=="], + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.2", "", { "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=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "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=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.3", "", { "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=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.1.6", "", { "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=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "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=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "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=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.3", "", { "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=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "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=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "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 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="], + "@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], + "@types/react": ["@types/react@19.0.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="], "@types/react-dom": ["@types/react-dom@19.0.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="], @@ -446,8 +477,14 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -478,6 +515,8 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], @@ -502,6 +541,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], @@ -536,7 +577,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.20.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=="], + "eslint": ["eslint@9.20.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=="], "eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="], @@ -574,6 +615,8 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], @@ -584,8 +627,12 @@ "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="], + "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], + "for-each": ["for-each@0.3.4", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw=="], + "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -604,7 +651,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@15.14.0", "", {}, "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="], + "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -778,6 +825,10 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minisearch": ["minisearch@7.1.1", "", {}, "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw=="], @@ -838,12 +889,16 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.5.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="], + "prettier": ["prettier@3.5.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw=="], "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "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=="], + "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -852,8 +907,12 @@ "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], + "react-dropzone": ["react-dropzone@14.3.5", "", { "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=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-number-format": ["react-number-format@5.4.3", "", { "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=="], + "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "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=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "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 @@ "sigma": ["sigma@3.0.1", "", { "dependencies": { "events": "^3.3.0", "graphology-utils": "^2.5.2" } }, "sha512-z67BX1FhIpD+wLs2WJ7QS2aR49TcSr3YaVZ2zU8cAc5jMiUYlSbeDp4EI6euBDUpm3/lzO4pfytP/gW4BhXWuA=="], + "sonner": ["sonner@1.7.4", "", { "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=="], + "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -936,6 +997,8 @@ "tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="], + "tailwind-scrollbar": ["tailwind-scrollbar@4.0.0", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-elqx9m09VHY8gkrMiyimFO09JlS3AyLFXT0eaLaWPi7ImwHlbZj1ce/AxSis2LtR+ewBGEyUV7URNEMcjP1Z2w=="], + "tailwindcss": ["tailwindcss@4.0.6", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], @@ -1006,12 +1069,16 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@types/ws/@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "babel-plugin-macros/resolve": ["resolve@1.22.10", "", { "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=="], + "bun-types/@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], diff --git a/lightrag_webui/eslint.config.js b/lightrag_webui/eslint.config.js index 9d6f8eb8ca85b307d79db685cb6244960e270d69..7f42e8b1b9dab7d6ab10fda8b807b50aa9206904 100644 --- a/lightrag_webui/eslint.config.js +++ b/lightrag_webui/eslint.config.js @@ -7,27 +7,30 @@ import tseslint from 'typescript-eslint' import prettier from 'eslint-config-prettier' import react from 'eslint-plugin-react' -export default tseslint.config({ ignores: ['dist'] }, prettier, { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx,js,jsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser - }, - settings: { react: { version: '19.0' } }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - '@stylistic/js': stylisticJs, - react - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], - ...react.configs.recommended.rules, - ...react.configs['jsx-runtime'].rules, - '@stylistic/js/indent': ['error', 2], - '@stylistic/js/quotes': ['error', 'single'], - '@typescript-eslint/no-explicit-any': ['off'] +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier], + files: ['**/*.{ts,tsx,js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser + }, + settings: { react: { version: '19.0' } }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + '@stylistic/js': stylisticJs, + react + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + '@stylistic/js/indent': ['error', 2], + '@stylistic/js/quotes': ['error', 'single'], + '@typescript-eslint/no-explicit-any': ['off'] + } } -}) +) diff --git a/lightrag_webui/index.html b/lightrag_webui/index.html index aa36e984336534ff6a12ad19d274875a83bbf1ec..0e019f0512af4cbe660bde7421116ca5bc8f5136 100644 --- a/lightrag_webui/index.html +++ b/lightrag_webui/index.html @@ -4,7 +4,7 @@ - Lightrag Graph Viewer + Lightrag
diff --git a/lightrag_webui/package.json b/lightrag_webui/package.json index f9fa65f444688d2f3a5b50842617ae4676cad490..26991d9cc6da56c403820332b9b1e3434b533228 100644 --- a/lightrag_webui/package.json +++ b/lightrag_webui/package.json @@ -10,13 +10,19 @@ "preview": "bunx --bun vite preview" }, "dependencies": { - "@faker-js/faker": "^9.4.0", + "@faker-js/faker": "^9.5.0", + "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-progress": "^1.1.2", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", + "@radix-ui/react-use-controllable-state": "^1.1.0", "@react-sigma/core": "^5.0.2", "@react-sigma/graph-search": "^5.0.3", "@react-sigma/layout-circlepack": "^5.0.2", @@ -28,6 +34,7 @@ "@react-sigma/minimap": "^5.0.2", "@sigma/edge-curve": "^3.1.0", "@sigma/node-border": "^3.0.0", + "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.4", @@ -37,9 +44,13 @@ "minisearch": "^7.1.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-dropzone": "^14.3.5", + "react-number-format": "^5.4.3", "seedrandom": "^3.0.5", "sigma": "^3.0.1", + "sonner": "^1.7.4", "tailwind-merge": "^3.0.1", + "tailwind-scrollbar": "^4.0.0", "zustand": "^5.0.3" }, "devDependencies": { @@ -47,19 +58,19 @@ "@stylistic/eslint-plugin-js": "^3.1.0", "@tailwindcss/vite": "^4.0.6", "@types/bun": "^1.2.2", - "@types/node": "^22.13.1", + "@types/node": "^22.13.4", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@types/seedrandom": "^3.0.8", "@vitejs/plugin-react-swc": "^3.8.0", - "eslint": "^9.20.0", + "eslint": "^9.20.1", "eslint-config-prettier": "^10.0.1", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.14.0", + "globals": "^15.15.0", "graphology-types": "^0.24.8", - "prettier": "^3.5.0", + "prettier": "^3.5.1", "prettier-plugin-tailwindcss": "^0.6.11", "tailwindcss": "^4.0.6", "tailwindcss-animate": "^1.0.7", diff --git a/lightrag_webui/src/App.tsx b/lightrag_webui/src/App.tsx index efdf4c9d15aeda218becdbe40e0dd59272e591d0..2c7e8001c2e4acff2c8048a8631db3d599b50c70 100644 --- a/lightrag_webui/src/App.tsx +++ b/lightrag_webui/src/App.tsx @@ -1,17 +1,30 @@ -import ThemeProvider from '@/components/ThemeProvider' +import { useState, useCallback } from 'react' +import ThemeProvider from '@/components/graph/ThemeProvider' import MessageAlert from '@/components/MessageAlert' -import StatusIndicator from '@/components/StatusIndicator' -import GraphViewer from '@/GraphViewer' +import ApiKeyAlert from '@/components/ApiKeyAlert' +import StatusIndicator from '@/components/graph/StatusIndicator' import { healthCheckInterval } from '@/lib/constants' import { useBackendState } from '@/stores/state' import { useSettingsStore } from '@/stores/settings' import { useEffect } from 'react' +import { Toaster } from 'sonner' +import SiteHeader from '@/features/SiteHeader' +import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag' + +import GraphViewer from '@/features/GraphViewer' +import DocumentManager from '@/features/DocumentManager' +import RetrievalTesting from '@/features/RetrievalTesting' +import ApiSite from '@/features/ApiSite' + +import { Tabs, TabsContent } from '@/components/ui/Tabs' function App() { const message = useBackendState.use.message() const enableHealthCheck = useSettingsStore.use.enableHealthCheck() + const [currentTab] = useState(() => useSettingsStore.getState().currentTab) + const [apiKeyInvalid, setApiKeyInvalid] = useState(false) - // health check + // Health check useEffect(() => { if (!enableHealthCheck) return @@ -24,13 +37,50 @@ function App() { return () => clearInterval(interval) }, [enableHealthCheck]) + const handleTabChange = useCallback( + (tab: string) => useSettingsStore.getState().setCurrentTab(tab as any), + [] + ) + + useEffect(() => { + if (message) { + if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) { + setApiKeyInvalid(true) + return + } + } + setApiKeyInvalid(false) + }, [message, setApiKeyInvalid]) + return ( -
- -
- {enableHealthCheck && } - {message !== null && } +
+ + +
+ + + + + + + + + + + + +
+
+ {enableHealthCheck && } + {message !== null && !apiKeyInvalid && } + {apiKeyInvalid && } + +
) } diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index a4e47b2f8d83cccbc6034b69e7a1a25bad427187..6308ef6281b0738a39ea6d3264e79c97ddd98888 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -1,3 +1,4 @@ +import axios, { AxiosError } from 'axios' import { backendBaseUrl } from '@/lib/constants' import { errorMessage } from '@/lib/utils' import { useSettingsStore } from '@/stores/settings' @@ -26,8 +27,6 @@ export type LightragStatus = { status: 'healthy' working_directory: string input_directory: string - indexed_files: string[] - indexed_files_count: number configuration: { llm_binding: string llm_binding_host: string @@ -51,94 +50,133 @@ export type LightragDocumentsScanProgress = { progress: number } +/** + * Specifies the retrieval mode: + * - "naive": Performs a basic search without advanced techniques. + * - "local": Focuses on context-dependent information. + * - "global": Utilizes global knowledge. + * - "hybrid": Combines local and global retrieval methods. + * - "mix": Integrates knowledge graph and vector retrieval. + */ export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix' +export type Message = { + role: 'user' | 'assistant' | 'system' + content: string +} + export type QueryRequest = { query: string + /** Specifies the retrieval mode. */ mode: QueryMode - stream?: boolean + /** If True, only returns the retrieved context without generating a response. */ only_need_context?: boolean + /** If True, only returns the generated prompt without producing a response. */ + only_need_prompt?: boolean + /** Defines the response format. Examples: 'Multiple Paragraphs', 'Single Paragraph', 'Bullet Points'. */ + response_type?: string + /** If True, enables streaming output for real-time responses. */ + stream?: boolean + /** Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode. */ + top_k?: number + /** Maximum number of tokens allowed for each retrieved text chunk. */ + max_token_for_text_unit?: number + /** Maximum number of tokens allocated for relationship descriptions in global retrieval. */ + max_token_for_global_context?: number + /** Maximum number of tokens allocated for entity descriptions in local retrieval. */ + max_token_for_local_context?: number + /** List of high-level keywords to prioritize in retrieval. */ + hl_keywords?: string[] + /** List of low-level keywords to refine retrieval focus. */ + ll_keywords?: string[] + /** + * Stores past conversation history to maintain context. + * Format: [{"role": "user/assistant", "content": "message"}]. + */ + conversation_history?: Message[] + /** Number of complete conversation turns (user-assistant pairs) to consider in the response context. */ + history_turns?: number } export type QueryResponse = { response: string } +export type DocActionResponse = { + status: 'success' | 'partial_success' | 'failure' + message: string +} + +export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed' + +export type DocStatusResponse = { + id: string + content_summary: string + content_length: number + status: DocStatus + created_at: string + updated_at: string + chunks_count?: number + error?: string + metadata?: Record +} + +export type DocsStatusesResponse = { + statuses: Record +} + export const InvalidApiKeyError = 'Invalid API Key' export const RequireApiKeError = 'API Key required' -// Helper functions -const getResponseContent = async (response: Response) => { - const contentType = response.headers.get('content-type') - if (contentType) { - if (contentType.includes('application/json')) { - const data = await response.json() - return JSON.stringify(data, undefined, 2) - } else if (contentType.startsWith('text/')) { - return await response.text() - } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) { - return await response.text() - } else if (contentType.includes('application/octet-stream')) { - const buffer = await response.arrayBuffer() - const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }) - return decoder.decode(buffer) - } else { - try { - return await response.text() - } catch (error) { - console.warn('Failed to decode as text, may be binary:', error) - return `[Could not decode response body. Content-Type: ${contentType}]` - } - } - } else { - try { - return await response.text() - } catch (error) { - console.warn('Failed to decode as text, may be binary:', error) - return '[Could not decode response body. No Content-Type header.]' - } +// Axios instance +const axiosInstance = axios.create({ + baseURL: backendBaseUrl, + headers: { + 'Content-Type': 'application/json' } - return '' -} +}) -const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise => { +// Interceptor:add api key +axiosInstance.interceptors.request.use((config) => { const apiKey = useSettingsStore.getState().apiKey - const headers = { - ...(options.headers || {}), - ...(apiKey ? { 'X-API-Key': apiKey } : {}) + if (apiKey) { + config.headers['X-API-Key'] = apiKey } + return config +}) - const response = await fetch(backendBaseUrl + url, { - ...options, - headers - }) - - if (!response.ok) { - throw new Error( - `${response.status} ${response.statusText}\n${await getResponseContent(response)}\n${response.url}` - ) +// Interceptor:hanle error +axiosInstance.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response) { + throw new Error( + `${error.response.status} ${error.response.statusText}\n${JSON.stringify( + error.response.data + )}\n${error.config?.url}` + ) + } + throw error } - - return response -} +) // API methods export const queryGraphs = async (label: string): Promise => { - const response = await fetchWithAuth(`/graphs?label=${label}`) - return await response.json() + const response = await axiosInstance.get(`/graphs?label=${label}`) + return response.data } export const getGraphLabels = async (): Promise => { - const response = await fetchWithAuth('/graph/label/list') - return await response.json() + const response = await axiosInstance.get('/graph/label/list') + return response.data } export const checkHealth = async (): Promise< LightragStatus | { status: 'error'; message: string } > => { try { - const response = await fetchWithAuth('/health') - return await response.json() + const response = await axiosInstance.get('/health') + return response.data } catch (e) { return { status: 'error', @@ -147,132 +185,132 @@ export const checkHealth = async (): Promise< } } -export const getDocuments = async (): Promise => { - const response = await fetchWithAuth('/documents') - return await response.json() +export const getDocuments = async (): Promise => { + const response = await axiosInstance.get('/documents') + return response.data } -export const getDocumentsScanProgress = async (): Promise => { - const response = await fetchWithAuth('/documents/scan-progress') - return await response.json() +export const scanNewDocuments = async (): Promise<{ status: string }> => { + const response = await axiosInstance.post('/documents/scan') + return response.data } -export const uploadDocument = async ( - file: File -): Promise<{ - status: string - message: string - total_documents: number -}> => { - const formData = new FormData() - formData.append('file', file) - - const response = await fetchWithAuth('/documents/upload', { - method: 'POST', - body: formData - }) - return await response.json() -} - -export const startDocumentScan = async (): Promise<{ status: string }> => { - const response = await fetchWithAuth('/documents/scan', { - method: 'POST' - }) - return await response.json() +export const getDocumentsScanProgress = async (): Promise => { + const response = await axiosInstance.get('/documents/scan-progress') + return response.data } export const queryText = async (request: QueryRequest): Promise => { - const response = await fetchWithAuth('/query', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }) - return await response.json() + const response = await axiosInstance.post('/query', request) + return response.data } -export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => { - const response = await fetchWithAuth('/query/stream', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(request) - }) +export const queryTextStream = async ( + request: QueryRequest, + onChunk: (chunk: string) => void, + onError?: (error: string) => void +) => { + try { + let buffer = '' + await axiosInstance.post('/query/stream', request, { + responseType: 'text', + headers: { + Accept: 'application/x-ndjson' + }, + transformResponse: [ + (data: string) => { + // Accumulate the data and process complete lines + buffer += data + const lines = buffer.split('\n') + // Keep the last potentially incomplete line in the buffer + buffer = lines.pop() || '' - const reader = response.body?.getReader() - if (!reader) throw new Error('No response body') - - const decoder = new TextDecoder() - while (true) { - const { done, value } = await reader.read() - if (done) break - - const chunk = decoder.decode(value) - const lines = chunk.split('\n') - for (const line of lines) { - if (line) { - try { - const data = JSON.parse(line) - if (data.response) { - onChunk(data.response) + for (const line of lines) { + if (line.trim()) { + try { + const parsed = JSON.parse(line) + if (parsed.response) { + onChunk(parsed.response) + } else if (parsed.error && onError) { + onError(parsed.error) + } + } catch (e) { + console.error('Error parsing stream chunk:', e) + if (onError) onError('Error parsing server response') + } + } } - } catch (e) { - console.error('Error parsing stream chunk:', e) + return data } + ] + }) + + // Process any remaining data in the buffer + if (buffer.trim()) { + try { + const parsed = JSON.parse(buffer) + if (parsed.response) { + onChunk(parsed.response) + } else if (parsed.error && onError) { + onError(parsed.error) + } + } catch (e) { + console.error('Error parsing final chunk:', e) + if (onError) onError('Error parsing server response') } } + } catch (error) { + const message = errorMessage(error) + console.error('Stream request failed:', message) + if (onError) onError(message) } } -// Text insertion API export const insertText = async ( text: string, description?: string -): Promise<{ - status: string - message: string - document_count: number -}> => { - const response = await fetchWithAuth('/documents/text', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ text, description }) - }) - return await response.json() +): Promise => { + const response = await axiosInstance.post('/documents/text', { text, description }) + return response.data } -// Batch file upload API -export const uploadBatchDocuments = async ( - files: File[] -): Promise<{ - status: string - message: string - document_count: number -}> => { +export const uploadDocument = async ( + file: File, + onUploadProgress?: (percentCompleted: number) => void +): Promise => { const formData = new FormData() - files.forEach((file) => { - formData.append('files', file) - }) + formData.append('file', file) - const response = await fetchWithAuth('/documents/batch', { - method: 'POST', - body: formData + const response = await axiosInstance.post('/documents/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + }, + // prettier-ignore + onUploadProgress: + onUploadProgress !== undefined + ? (progressEvent) => { + const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total!) + onUploadProgress(percentCompleted) + } + : undefined }) - return await response.json() + return response.data } -// Clear all documents API -export const clearDocuments = async (): Promise<{ - status: string - message: string - document_count: number -}> => { - const response = await fetchWithAuth('/documents', { - method: 'DELETE' - }) - return await response.json() +export const batchUploadDocuments = async ( + files: File[], + onUploadProgress?: (fileName: string, percentCompleted: number) => void +): Promise => { + return await Promise.all( + files.map(async (file) => { + return await uploadDocument(file, (percentCompleted) => { + onUploadProgress?.(file.name, percentCompleted) + }) + }) + ) +} + +export const clearDocuments = async (): Promise => { + const response = await axiosInstance.delete('/documents') + return response.data } diff --git a/lightrag_webui/src/components/ApiKeyAlert.tsx b/lightrag_webui/src/components/ApiKeyAlert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..86c8e13f5ac052496b57581f6db14c8dd2661b8f --- /dev/null +++ b/lightrag_webui/src/components/ApiKeyAlert.tsx @@ -0,0 +1,77 @@ +import { useState, useCallback, useEffect } from 'react' +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/AlertDialog' +import Button from '@/components/ui/Button' +import Input from '@/components/ui/Input' +import { useSettingsStore } from '@/stores/settings' +import { useBackendState } from '@/stores/state' +import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag' + +import { toast } from 'sonner' + +const ApiKeyAlert = () => { + const [opened, setOpened] = useState(true) + const apiKey = useSettingsStore.use.apiKey() + const [tempApiKey, setTempApiKey] = useState('') + const message = useBackendState.use.message() + + useEffect(() => { + setTempApiKey(apiKey || '') + }, [apiKey, opened]) + + useEffect(() => { + if (message) { + if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) { + setOpened(true) + } + } + }, [message, setOpened]) + + const setApiKey = useCallback(async () => { + useSettingsStore.setState({ apiKey: tempApiKey || null }) + if (await useBackendState.getState().check()) { + setOpened(false) + return + } + toast.error('API Key is invalid') + }, [tempApiKey]) + + const handleTempApiKeyChange = useCallback( + (e: React.ChangeEvent) => { + setTempApiKey(e.target.value) + }, + [setTempApiKey] + ) + + return ( + + + + API Key is required + Please enter your API key + +
e.preventDefault()}> + + + +
+
+
+ ) +} + +export default ApiKeyAlert diff --git a/lightrag_webui/src/components/MessageAlert.tsx b/lightrag_webui/src/components/MessageAlert.tsx index 6a25baabeed97fc1bc8a01e6615b30cae60218cf..cd23bbd92ecbbed8dbf817e44bdfa306032dec34 100644 --- a/lightrag_webui/src/components/MessageAlert.tsx +++ b/lightrag_webui/src/components/MessageAlert.tsx @@ -22,10 +22,11 @@ const MessageAlert = () => { return ( {!health && ( @@ -42,7 +43,7 @@ const MessageAlert = () => { + + e.preventDefault()}> + + Clear documents + Do you really want to clear all documents? + + + + + ) +} diff --git a/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..181b52f182ad02d6dbc380d035db6a9ef51df6d8 --- /dev/null +++ b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx @@ -0,0 +1,91 @@ +import { useState, useCallback } from 'react' +import Button from '@/components/ui/Button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from '@/components/ui/Dialog' +import FileUploader from '@/components/ui/FileUploader' +import { toast } from 'sonner' +import { errorMessage } from '@/lib/utils' +import { uploadDocument } from '@/api/lightrag' + +import { UploadIcon } from 'lucide-react' + +export default function UploadDocumentsDialog() { + const [open, setOpen] = useState(false) + const [isUploading, setIsUploading] = useState(false) + const [progresses, setProgresses] = useState>({}) + + const handleDocumentsUpload = useCallback( + async (filesToUpload: File[]) => { + setIsUploading(true) + + try { + await Promise.all( + filesToUpload.map(async (file) => { + try { + const result = await uploadDocument(file, (percentCompleted: number) => { + console.debug(`Uploading ${file.name}: ${percentCompleted}%`) + setProgresses((pre) => ({ + ...pre, + [file.name]: percentCompleted + })) + }) + if (result.status === 'success') { + toast.success(`Upload Success:\n${file.name} uploaded successfully`) + } else { + toast.error(`Upload Failed:\n${file.name}\n${result.message}`) + } + } catch (err) { + toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`) + } + }) + ) + } catch (err) { + toast.error('Upload Failed\n' + errorMessage(err)) + } finally { + setIsUploading(false) + // setOpen(false) + } + }, + [setIsUploading, setProgresses] + ) + + return ( + { + if (isUploading && !open) { + return + } + setOpen(open) + }} + > + + + + e.preventDefault()}> + + Upload documents + + Drag and drop your documents here or click to browse. + + + + + + ) +} diff --git a/lightrag_webui/src/components/FocusOnNode.tsx b/lightrag_webui/src/components/graph/FocusOnNode.tsx similarity index 100% rename from lightrag_webui/src/components/FocusOnNode.tsx rename to lightrag_webui/src/components/graph/FocusOnNode.tsx diff --git a/lightrag_webui/src/components/FullScreenControl.tsx b/lightrag_webui/src/components/graph/FullScreenControl.tsx similarity index 100% rename from lightrag_webui/src/components/FullScreenControl.tsx rename to lightrag_webui/src/components/graph/FullScreenControl.tsx diff --git a/lightrag_webui/src/components/GraphControl.tsx b/lightrag_webui/src/components/graph/GraphControl.tsx similarity index 100% rename from lightrag_webui/src/components/GraphControl.tsx rename to lightrag_webui/src/components/graph/GraphControl.tsx diff --git a/lightrag_webui/src/components/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx similarity index 100% rename from lightrag_webui/src/components/GraphLabels.tsx rename to lightrag_webui/src/components/graph/GraphLabels.tsx diff --git a/lightrag_webui/src/components/GraphSearch.tsx b/lightrag_webui/src/components/graph/GraphSearch.tsx similarity index 100% rename from lightrag_webui/src/components/GraphSearch.tsx rename to lightrag_webui/src/components/graph/GraphSearch.tsx diff --git a/lightrag_webui/src/components/LayoutsControl.tsx b/lightrag_webui/src/components/graph/LayoutsControl.tsx similarity index 100% rename from lightrag_webui/src/components/LayoutsControl.tsx rename to lightrag_webui/src/components/graph/LayoutsControl.tsx diff --git a/lightrag_webui/src/components/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx similarity index 100% rename from lightrag_webui/src/components/PropertiesView.tsx rename to lightrag_webui/src/components/graph/PropertiesView.tsx diff --git a/lightrag_webui/src/components/Settings.tsx b/lightrag_webui/src/components/graph/Settings.tsx similarity index 97% rename from lightrag_webui/src/components/Settings.tsx rename to lightrag_webui/src/components/graph/Settings.tsx index 45bc673120f7a3c86279aaba80a44e296a03cca3..3a6cc51c7b7effb39eb9eac3217b780879184b15 100644 --- a/lightrag_webui/src/components/Settings.tsx +++ b/lightrag_webui/src/components/graph/Settings.tsx @@ -1,5 +1,5 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' -import { Checkbox } from '@/components/ui/Checkbox' +import Checkbox from '@/components/ui/Checkbox' import Button from '@/components/ui/Button' import Separator from '@/components/ui/Separator' import Input from '@/components/ui/Input' @@ -40,7 +40,7 @@ const LabeledCheckBox = ({ */ export default function Settings() { const [opened, setOpened] = useState(false) - const [tempApiKey, setTempApiKey] = useState('') // 用于临时存储输入的API Key + const [tempApiKey, setTempApiKey] = useState('') const showPropertyPanel = useSettingsStore.use.showPropertyPanel() const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() diff --git a/lightrag_webui/src/components/StatusCard.tsx b/lightrag_webui/src/components/graph/StatusCard.tsx similarity index 96% rename from lightrag_webui/src/components/StatusCard.tsx rename to lightrag_webui/src/components/graph/StatusCard.tsx index ea038c740394ff65263e5fd25724326dbfb4a893..3084d10376807f492a0a050a6c977ca3c1abc4d2 100644 --- a/lightrag_webui/src/components/StatusCard.tsx +++ b/lightrag_webui/src/components/graph/StatusCard.tsx @@ -14,8 +14,6 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => { {status.working_directory} Input Directory: {status.input_directory} - Indexed Files: - {status.indexed_files_count} diff --git a/lightrag_webui/src/components/StatusIndicator.tsx b/lightrag_webui/src/components/graph/StatusIndicator.tsx similarity index 96% rename from lightrag_webui/src/components/StatusIndicator.tsx rename to lightrag_webui/src/components/graph/StatusIndicator.tsx index 25d0032c5d601167a25299e7ac84b3f02a491855..3272d9fa73dbc2401986c7b6b499eb1c1efc3d40 100644 --- a/lightrag_webui/src/components/StatusIndicator.tsx +++ b/lightrag_webui/src/components/graph/StatusIndicator.tsx @@ -2,7 +2,7 @@ import { cn } from '@/lib/utils' import { useBackendState } from '@/stores/state' import { useEffect, useState } from 'react' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' -import StatusCard from '@/components/StatusCard' +import StatusCard from '@/components/graph/StatusCard' const StatusIndicator = () => { const health = useBackendState.use.health() diff --git a/lightrag_webui/src/components/ThemeProvider.tsx b/lightrag_webui/src/components/graph/ThemeProvider.tsx similarity index 100% rename from lightrag_webui/src/components/ThemeProvider.tsx rename to lightrag_webui/src/components/graph/ThemeProvider.tsx diff --git a/lightrag_webui/src/components/ThemeToggle.tsx b/lightrag_webui/src/components/graph/ThemeToggle.tsx similarity index 95% rename from lightrag_webui/src/components/ThemeToggle.tsx rename to lightrag_webui/src/components/graph/ThemeToggle.tsx index 6b66b2b328650099f8c659439a1f69919fb9b0d8..8e92d862ddb71fa84d5276d5edeedd12c12c3901 100644 --- a/lightrag_webui/src/components/ThemeToggle.tsx +++ b/lightrag_webui/src/components/graph/ThemeToggle.tsx @@ -19,6 +19,7 @@ export default function ThemeToggle() { variant={controlButtonVariant} tooltip="Switch to light theme" size="icon" + side="bottom" > @@ -30,6 +31,7 @@ export default function ThemeToggle() { variant={controlButtonVariant} tooltip="Switch to dark theme" size="icon" + side="bottom" > diff --git a/lightrag_webui/src/components/ZoomControl.tsx b/lightrag_webui/src/components/graph/ZoomControl.tsx similarity index 100% rename from lightrag_webui/src/components/ZoomControl.tsx rename to lightrag_webui/src/components/graph/ZoomControl.tsx diff --git a/lightrag_webui/src/components/retrieval/QuerySettings.tsx b/lightrag_webui/src/components/retrieval/QuerySettings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bef348578fda7cc231469cb85d6cc92d8ee5d9e1 --- /dev/null +++ b/lightrag_webui/src/components/retrieval/QuerySettings.tsx @@ -0,0 +1,279 @@ +import { useCallback } from 'react' +import { QueryMode, QueryRequest } from '@/api/lightrag' +import Text from '@/components/ui/Text' +import Input from '@/components/ui/Input' +import Checkbox from '@/components/ui/Checkbox' +import NumberInput from '@/components/ui/NumberInput' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card' +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/Select' +import { useSettingsStore } from '@/stores/settings' + +export default function QuerySettings() { + const querySettings = useSettingsStore((state) => state.querySettings) + + const handleChange = useCallback((key: keyof QueryRequest, value: any) => { + useSettingsStore.getState().updateQuerySettings({ [key]: value }) + }, []) + + return ( + + + Parameters + Configure your query parameters + + +
+
+ {/* Query Mode */} + <> + + + + + {/* Response Format */} + <> + + + + + {/* Top K */} + <> + + handleChange('top_k', v)} + min={1} + placeholder="Number of results" + /> + + + {/* Max Tokens */} + <> + <> + + handleChange('max_token_for_text_unit', v)} + min={1} + placeholder="Max tokens for text unit" + /> + + + <> + + handleChange('max_token_for_global_context', v)} + min={1} + placeholder="Max tokens for global context" + /> + + + <> + + handleChange('max_token_for_local_context', v)} + min={1} + placeholder="Max tokens for local context" + /> + + + + {/* History Turns */} + <> + + handleChange('history_turns', v)} + min={0} + placeholder="Number of history turns" + /> + + + {/* Keywords */} + <> + <> + + { + const keywords = e.target.value + .split(',') + .map((k) => k.trim()) + .filter((k) => k !== '') + handleChange('hl_keywords', keywords) + }} + placeholder="Enter keywords" + /> + + + <> + + { + const keywords = e.target.value + .split(',') + .map((k) => k.trim()) + .filter((k) => k !== '') + handleChange('ll_keywords', keywords) + }} + placeholder="Enter keywords" + /> + + + + {/* Toggle Options */} + <> +
+ +
+ handleChange('only_need_context', checked)} + /> +
+ +
+ +
+ handleChange('only_need_prompt', checked)} + /> +
+ +
+ +
+ handleChange('stream', checked)} + /> +
+ +
+
+ + + ) +} diff --git a/lightrag_webui/src/components/ui/AlertDialog.tsx b/lightrag_webui/src/components/ui/AlertDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..36868fcb95e5f905c7df41306c4d27692d3ac8f8 --- /dev/null +++ b/lightrag_webui/src/components/ui/AlertDialog.tsx @@ -0,0 +1,115 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/Button' + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel +} diff --git a/lightrag_webui/src/components/ui/AsyncSearch.tsx b/lightrag_webui/src/components/ui/AsyncSearch.tsx index c417fd6b09ccc70dddad1235113a2f82ff166a6c..a9b3a6cea65c4a785ca6531b844a677b34805850 100644 --- a/lightrag_webui/src/components/ui/AsyncSearch.tsx +++ b/lightrag_webui/src/components/ui/AsyncSearch.tsx @@ -193,7 +193,7 @@ export function AsyncSearch({
)}
-