diff --git a/README-zh.md b/README-zh.md index 784fd1f2..d345562f 100644 --- a/README-zh.md +++ b/README-zh.md @@ -11,7 +11,6 @@ - [X] [2024.12.31]🎯📢LightRAG现在支持[通过文档ID删除](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete)。 - [X] [2024.11.25]🎯📢LightRAG现在支持无缝集成[自定义知识图谱](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg),使用户能够用自己的领域专业知识增强系统。 - [X] [2024.11.19]🎯📢LightRAG的综合指南现已在[LearnOpenCV](https://learnopencv.com/lightrag)上发布。非常感谢博客作者。 -- [X] [2024.11.12]🎯📢LightRAG现在支持[Oracle Database 23ai的所有存储类型(KV、向量和图)](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_oracle_demo.py)。 - [X] [2024.11.11]🎯📢LightRAG现在支持[通过实体名称删除实体](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete)。 - [X] [2024.11.09]🎯📢推出[LightRAG Gui](https://lightrag-gui.streamlit.app),允许您插入、查询、可视化和下载LightRAG知识。 - [X] [2024.11.04]🎯📢现在您可以[使用Neo4J进行存储](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage)。 @@ -410,6 +409,54 @@ if __name__ == "__main__": +### Token统计功能 +
+ 概述和使用 + +LightRAG提供了TokenTracker工具来跟踪和管理大模型的token消耗。这个功能对于控制API成本和优化性能特别有用。 + +#### 使用方法 + +```python +from lightrag.utils import TokenTracker + +# 创建TokenTracker实例 +token_tracker = TokenTracker() + +# 方法1:使用上下文管理器(推荐) +# 适用于需要自动跟踪token使用的场景 +with token_tracker: + result1 = await llm_model_func("你的问题1") + result2 = await llm_model_func("你的问题2") + +# 方法2:手动添加token使用记录 +# 适用于需要更精细控制token统计的场景 +token_tracker.reset() + +rag.insert() + +rag.query("你的问题1", param=QueryParam(mode="naive")) +rag.query("你的问题2", param=QueryParam(mode="mix")) + +# 显示总token使用量(包含插入和查询操作) +print("Token usage:", token_tracker.get_usage()) +``` + +#### 使用建议 +- 在长会话或批量操作中使用上下文管理器,可以自动跟踪所有token消耗 +- 对于需要分段统计的场景,使用手动模式并适时调用reset() +- 定期检查token使用情况,有助于及时发现异常消耗 +- 在开发测试阶段积极使用此功能,以便优化生产环境的成本 + +#### 实际应用示例 +您可以参考以下示例来实现token统计: +- `examples/lightrag_gemini_track_token_demo.py`:使用Google Gemini模型的token统计示例 +- `examples/lightrag_siliconcloud_track_token_demo.py`:使用SiliconCloud模型的token统计示例 + +这些示例展示了如何在不同模型和场景下有效地使用TokenTracker功能。 + +
+ ### 对话历史 LightRAG现在通过对话历史功能支持多轮对话。以下是使用方法: @@ -1037,9 +1084,10 @@ rag.clear_cache(modes=["local"]) | **参数** | **类型** | **说明** | **默认值** | |--------------|----------|-----------------|-------------| | **working_dir** | `str` | 存储缓存的目录 | `lightrag_cache+timestamp` | -| **kv_storage** | `str` | 文档和文本块的存储类型。支持的类型:`JsonKVStorage`、`OracleKVStorage` | `JsonKVStorage` | -| **vector_storage** | `str` | 嵌入向量的存储类型。支持的类型:`NanoVectorDBStorage`、`OracleVectorDBStorage` | `NanoVectorDBStorage` | -| **graph_storage** | `str` | 图边和节点的存储类型。支持的类型:`NetworkXStorage`、`Neo4JStorage`、`OracleGraphStorage` | `NetworkXStorage` | +| **kv_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`,`PGKVStorage`,`RedisKVStorage`,`MongoKVStorage` | `JsonKVStorage` | +| **vector_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`,`PGVectorStorage`,`MilvusVectorDBStorage`,`ChromaVectorDBStorage`,`FaissVectorDBStorage`,`MongoVectorDBStorage`,`QdrantVectorDBStorage` | `NanoVectorDBStorage` | +| **graph_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`,`Neo4JStorage`,`PGGraphStorage`,`AGEStorage` | `NetworkXStorage` | +| **doc_status_storage** | `str` | Storage type for documents process status. Supported types: `JsonDocStatusStorage`,`PGDocStatusStorage`,`MongoDocStatusStorage` | `JsonDocStatusStorage` | | **chunk_token_size** | `int` | 拆分文档时每个块的最大令牌大小 | `1200` | | **chunk_overlap_token_size** | `int` | 拆分文档时两个块之间的重叠令牌大小 | `100` | | **tiktoken_model_name** | `str` | 用于计算令牌数的Tiktoken编码器的模型名称 | `gpt-4o-mini` | diff --git a/README.md b/README.md index 6c8861a2..091b5b77 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ - [X] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete). - [X] [2024.11.25]🎯📢LightRAG now supports seamless integration of [custom knowledge graphs](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg), empowering users to enhance the system with their own domain expertise. - [X] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author. -- [X] [2024.11.12]🎯📢LightRAG now supports [Oracle Database 23ai for all storage types (KV, vector, and graph)](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_oracle_demo.py). - [X] [2024.11.11]🎯📢LightRAG now supports [deleting entities by their names](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete). - [X] [2024.11.09]🎯📢Introducing the [LightRAG Gui](https://lightrag-gui.streamlit.app), which allows you to insert, query, visualize, and download LightRAG knowledge. - [X] [2024.11.04]🎯📢You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage). @@ -443,6 +442,55 @@ if __name__ == "__main__": +### Token Usage Tracking + +
+ Overview and Usage + +LightRAG provides a TokenTracker tool to monitor and manage token consumption by large language models. This feature is particularly useful for controlling API costs and optimizing performance. + +#### Usage + +```python +from lightrag.utils import TokenTracker + +# Create TokenTracker instance +token_tracker = TokenTracker() + +# Method 1: Using context manager (Recommended) +# Suitable for scenarios requiring automatic token usage tracking +with token_tracker: + result1 = await llm_model_func("your question 1") + result2 = await llm_model_func("your question 2") + +# Method 2: Manually adding token usage records +# Suitable for scenarios requiring more granular control over token statistics +token_tracker.reset() + +rag.insert() + +rag.query("your question 1", param=QueryParam(mode="naive")) +rag.query("your question 2", param=QueryParam(mode="mix")) + +# Display total token usage (including insert and query operations) +print("Token usage:", token_tracker.get_usage()) +``` + +#### Usage Tips +- Use context managers for long sessions or batch operations to automatically track all token consumption +- For scenarios requiring segmented statistics, use manual mode and call reset() when appropriate +- Regular checking of token usage helps detect abnormal consumption early +- Actively use this feature during development and testing to optimize production costs + +#### Practical Examples +You can refer to these examples for implementing token tracking: +- `examples/lightrag_gemini_track_token_demo.py`: Token tracking example using Google Gemini model +- `examples/lightrag_siliconcloud_track_token_demo.py`: Token tracking example using SiliconCloud model + +These examples demonstrate how to effectively use the TokenTracker feature with different models and scenarios. + +
+ ### Conversation History Support @@ -607,7 +655,7 @@ The `apipeline_enqueue_documents` and `apipeline_process_enqueue_documents` func This is useful for scenarios where you want to process documents in the background while still allowing the main thread to continue executing. -And using a routine to process news documents. +And using a routine to process new documents. ```python rag = LightRAG(..) @@ -1096,9 +1144,10 @@ Valid modes are: | **Parameter** | **Type** | **Explanation** | **Default** | |--------------|----------|-----------------|-------------| | **working_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` | -| **kv_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` | -| **vector_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` | -| **graph_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` | +| **kv_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`,`PGKVStorage`,`RedisKVStorage`,`MongoKVStorage` | `JsonKVStorage` | +| **vector_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`,`PGVectorStorage`,`MilvusVectorDBStorage`,`ChromaVectorDBStorage`,`FaissVectorDBStorage`,`MongoVectorDBStorage`,`QdrantVectorDBStorage` | `NanoVectorDBStorage` | +| **graph_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`,`Neo4JStorage`,`PGGraphStorage`,`AGEStorage` | `NetworkXStorage` | +| **doc_status_storage** | `str` | Storage type for documents process status. Supported types: `JsonDocStatusStorage`,`PGDocStatusStorage`,`MongoDocStatusStorage` | `JsonDocStatusStorage` | | **chunk_token_size** | `int` | Maximum token size per chunk when splitting documents | `1200` | | **chunk_overlap_token_size** | `int` | Overlap token size between two chunks when splitting documents | `100` | | **tiktoken_model_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` | diff --git a/config.ini.example b/config.ini.example index 3041611e..5ff7cfbb 100644 --- a/config.ini.example +++ b/config.ini.example @@ -13,23 +13,6 @@ uri=redis://localhost:6379/1 [qdrant] uri = http://localhost:16333 -[oracle] -dsn = localhost:1521/XEPDB1 -user = your_username -password = your_password -config_dir = /path/to/oracle/config -wallet_location = /path/to/wallet # 可选 -wallet_password = your_wallet_password # 可选 -workspace = default # 可选,默认为default - -[tidb] -host = localhost -port = 4000 -user = your_username -password = your_password -database = your_database -workspace = default # 可选,默认为default - [postgres] host = localhost port = 5432 diff --git a/env.example b/env.example index 20d80d43..d21bbef6 100644 --- a/env.example +++ b/env.example @@ -4,11 +4,9 @@ # HOST=0.0.0.0 # PORT=9621 # WORKERS=2 -### separating data from difference Lightrag instances -# NAMESPACE_PREFIX=lightrag -### Max nodes return from grap retrieval -# MAX_GRAPH_NODES=1000 # CORS_ORIGINS=http://localhost:3000,http://localhost:8080 +WEBUI_TITLE='Graph RAG Engine' +WEBUI_DESCRIPTION="Simple and Fast Graph Based RAG System" ### Optional SSL Configuration # SSL=true @@ -22,6 +20,9 @@ ### Ollama Emulating Model Tag # OLLAMA_EMULATING_MODEL_TAG=latest +### Max nodes return from grap retrieval +# MAX_GRAPH_NODES=1000 + ### Logging level # LOG_LEVEL=INFO # VERBOSE=False @@ -110,24 +111,14 @@ LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage LIGHTRAG_GRAPH_STORAGE=NetworkXStorage LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage -### Oracle Database Configuration -ORACLE_DSN=localhost:1521/XEPDB1 -ORACLE_USER=your_username -ORACLE_PASSWORD='your_password' -ORACLE_CONFIG_DIR=/path/to/oracle/config -#ORACLE_WALLET_LOCATION=/path/to/wallet -#ORACLE_WALLET_PASSWORD='your_password' -### separating all data from difference Lightrag instances(deprecating, use NAMESPACE_PREFIX in future) -#ORACLE_WORKSPACE=default - -### TiDB Configuration -TIDB_HOST=localhost -TIDB_PORT=4000 -TIDB_USER=your_username -TIDB_PASSWORD='your_password' -TIDB_DATABASE=your_database -### separating all data from difference Lightrag instances(deprecating, use NAMESPACE_PREFIX in future) -#TIDB_WORKSPACE=default +### TiDB Configuration (Deprecated) +# TIDB_HOST=localhost +# TIDB_PORT=4000 +# TIDB_USER=your_username +# TIDB_PASSWORD='your_password' +# TIDB_DATABASE=your_database +### separating all data from difference Lightrag instances(deprecating) +# TIDB_WORKSPACE=default ### PostgreSQL Configuration POSTGRES_HOST=localhost @@ -135,8 +126,8 @@ POSTGRES_PORT=5432 POSTGRES_USER=your_username POSTGRES_PASSWORD='your_password' POSTGRES_DATABASE=your_database -### separating all data from difference Lightrag instances(deprecating, use NAMESPACE_PREFIX in future) -#POSTGRES_WORKSPACE=default +### separating all data from difference Lightrag instances(deprecating) +# POSTGRES_WORKSPACE=default ### Independent AGM Configuration(not for AMG embedded in PostreSQL) AGE_POSTGRES_DB= @@ -145,8 +136,8 @@ AGE_POSTGRES_PASSWORD= AGE_POSTGRES_HOST= # AGE_POSTGRES_PORT=8529 -### separating all data from difference Lightrag instances(deprecating, use NAMESPACE_PREFIX in future) # AGE Graph Name(apply to PostgreSQL and independent AGM) +### AGE_GRAPH_NAME is precated # AGE_GRAPH_NAME=lightrag ### Neo4j Configuration @@ -157,7 +148,7 @@ NEO4J_PASSWORD='your_password' ### MongoDB Configuration MONGO_URI=mongodb://root:root@localhost:27017/ MONGO_DATABASE=LightRAG -### separating all data from difference Lightrag instances(deprecating, use NAMESPACE_PREFIX in future) +### separating all data from difference Lightrag instances(deprecating) # MONGODB_GRAPH=false ### Milvus Configuration @@ -177,7 +168,9 @@ REDIS_URI=redis://localhost:6379 ### For JWT Auth # AUTH_ACCOUNTS='admin:admin123,user1:pass456' # TOKEN_SECRET=Your-Key-For-LightRAG-API-Server -# TOKEN_EXPIRE_HOURS=4 +# TOKEN_EXPIRE_HOURS=48 +# GUEST_TOKEN_EXPIRE_HOURS=24 +# JWT_ALGORITHM=HS256 ### API-Key to access LightRAG Server API # LIGHTRAG_API_KEY=your-secure-api-key-here diff --git a/examples/lightrag_api_ollama_demo.py b/examples/lightrag_api_ollama_demo.py deleted file mode 100644 index dad2a2e0..00000000 --- a/examples/lightrag_api_ollama_demo.py +++ /dev/null @@ -1,188 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from contextlib import asynccontextmanager -from pydantic import BaseModel -import os -from lightrag import LightRAG, QueryParam -from lightrag.llm.ollama import ollama_embed, ollama_model_complete -from lightrag.utils import EmbeddingFunc -from typing import Optional -import asyncio -import nest_asyncio -import aiofiles -from lightrag.kg.shared_storage import initialize_pipeline_status - -# Apply nest_asyncio to solve event loop issues -nest_asyncio.apply() - -DEFAULT_RAG_DIR = "index_default" - -DEFAULT_INPUT_FILE = "book.txt" -INPUT_FILE = os.environ.get("INPUT_FILE", f"{DEFAULT_INPUT_FILE}") -print(f"INPUT_FILE: {INPUT_FILE}") - -# Configure working directory -WORKING_DIR = os.environ.get("RAG_DIR", f"{DEFAULT_RAG_DIR}") -print(f"WORKING_DIR: {WORKING_DIR}") - - -if not os.path.exists(WORKING_DIR): - os.mkdir(WORKING_DIR) - - -async def init(): - rag = LightRAG( - working_dir=WORKING_DIR, - llm_model_func=ollama_model_complete, - llm_model_name="gemma2:9b", - llm_model_max_async=4, - llm_model_max_token_size=8192, - llm_model_kwargs={ - "host": "http://localhost:11434", - "options": {"num_ctx": 8192}, - }, - embedding_func=EmbeddingFunc( - embedding_dim=768, - max_token_size=8192, - func=lambda texts: ollama_embed( - texts, embed_model="nomic-embed-text", host="http://localhost:11434" - ), - ), - ) - - # Add initialization code - await rag.initialize_storages() - await initialize_pipeline_status() - - return rag - - -@asynccontextmanager -async def lifespan(app: FastAPI): - global rag - rag = await init() - print("done!") - yield - - -app = FastAPI( - title="LightRAG API", description="API for RAG operations", lifespan=lifespan -) - - -# Data models -class QueryRequest(BaseModel): - query: str - mode: str = "hybrid" - only_need_context: bool = False - - -class InsertRequest(BaseModel): - text: str - - -class Response(BaseModel): - status: str - data: Optional[str] = None - message: Optional[str] = None - - -# API routes -@app.post("/query", response_model=Response) -async def query_endpoint(request: QueryRequest): - try: - loop = asyncio.get_event_loop() - result = await loop.run_in_executor( - None, - lambda: rag.query( - request.query, - param=QueryParam( - mode=request.mode, only_need_context=request.only_need_context - ), - ), - ) - return Response(status="success", data=result) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -# insert by text -@app.post("/insert", response_model=Response) -async def insert_endpoint(request: InsertRequest): - try: - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: rag.insert(request.text)) - return Response(status="success", message="Text inserted successfully") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -# insert by file in payload -@app.post("/insert_file", response_model=Response) -async def insert_file(file: UploadFile = File(...)): - try: - file_content = await file.read() - # Read file content - try: - content = file_content.decode("utf-8") - except UnicodeDecodeError: - # If UTF-8 decoding fails, try other encodings - content = file_content.decode("gbk") - # Insert file content - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: rag.insert(content)) - - return Response( - status="success", - message=f"File content from {file.filename} inserted successfully", - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -# insert by local default file -@app.post("/insert_default_file", response_model=Response) -@app.get("/insert_default_file", response_model=Response) -async def insert_default_file(): - try: - # Read file content from book.txt - async with aiofiles.open(INPUT_FILE, "r", encoding="utf-8") as file: - content = await file.read() - print(f"read input file {INPUT_FILE} successfully") - # Insert file content - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: rag.insert(content)) - - return Response( - status="success", - message=f"File content from {INPUT_FILE} inserted successfully", - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/health") -async def health_check(): - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=8020) - -# Usage example -# To run the server, use the following command in your terminal: -# python lightrag_api_openai_compatible_demo.py - -# Example requests: -# 1. Query: -# curl -X POST "http://127.0.0.1:8020/query" -H "Content-Type: application/json" -d '{"query": "your query here", "mode": "hybrid"}' - -# 2. Insert text: -# curl -X POST "http://127.0.0.1:8020/insert" -H "Content-Type: application/json" -d '{"text": "your text here"}' - -# 3. Insert file: -# curl -X POST "http://127.0.0.1:8020/insert_file" -H "Content-Type: multipart/form-data" -F "file=@path/to/your/file.txt" - -# 4. Health check: -# curl -X GET "http://127.0.0.1:8020/health" diff --git a/examples/lightrag_api_openai_compatible_demo.py b/examples/lightrag_api_openai_compatible_demo.py deleted file mode 100644 index 312be872..00000000 --- a/examples/lightrag_api_openai_compatible_demo.py +++ /dev/null @@ -1,204 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from contextlib import asynccontextmanager -from pydantic import BaseModel -import os -from lightrag import LightRAG, QueryParam -from lightrag.llm.openai import openai_complete_if_cache, openai_embed -from lightrag.utils import EmbeddingFunc -import numpy as np -from typing import Optional -import asyncio -import nest_asyncio -from lightrag.kg.shared_storage import initialize_pipeline_status - -# Apply nest_asyncio to solve event loop issues -nest_asyncio.apply() - -DEFAULT_RAG_DIR = "index_default" -app = FastAPI(title="LightRAG API", description="API for RAG operations") - -# Configure working directory -WORKING_DIR = os.environ.get("RAG_DIR", f"{DEFAULT_RAG_DIR}") -print(f"WORKING_DIR: {WORKING_DIR}") -LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-4o-mini") -print(f"LLM_MODEL: {LLM_MODEL}") -EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "text-embedding-3-large") -print(f"EMBEDDING_MODEL: {EMBEDDING_MODEL}") -EMBEDDING_MAX_TOKEN_SIZE = int(os.environ.get("EMBEDDING_MAX_TOKEN_SIZE", 8192)) -print(f"EMBEDDING_MAX_TOKEN_SIZE: {EMBEDDING_MAX_TOKEN_SIZE}") -BASE_URL = os.environ.get("BASE_URL", "https://api.openai.com/v1") -print(f"BASE_URL: {BASE_URL}") -API_KEY = os.environ.get("API_KEY", "xxxxxxxx") -print(f"API_KEY: {API_KEY}") - -if not os.path.exists(WORKING_DIR): - os.mkdir(WORKING_DIR) - - -# LLM model function - - -async def llm_model_func( - prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs -) -> str: - return await openai_complete_if_cache( - model=LLM_MODEL, - prompt=prompt, - system_prompt=system_prompt, - history_messages=history_messages, - base_url=BASE_URL, - api_key=API_KEY, - **kwargs, - ) - - -# Embedding function - - -async def embedding_func(texts: list[str]) -> np.ndarray: - return await openai_embed( - texts=texts, - model=EMBEDDING_MODEL, - base_url=BASE_URL, - api_key=API_KEY, - ) - - -async def get_embedding_dim(): - test_text = ["This is a test sentence."] - embedding = await embedding_func(test_text) - embedding_dim = embedding.shape[1] - print(f"{embedding_dim=}") - return embedding_dim - - -# Initialize RAG instance -async def init(): - embedding_dimension = await get_embedding_dim() - - rag = LightRAG( - working_dir=WORKING_DIR, - llm_model_func=llm_model_func, - embedding_func=EmbeddingFunc( - embedding_dim=embedding_dimension, - max_token_size=EMBEDDING_MAX_TOKEN_SIZE, - func=embedding_func, - ), - ) - - await rag.initialize_storages() - await initialize_pipeline_status() - - return rag - - -@asynccontextmanager -async def lifespan(app: FastAPI): - global rag - rag = await init() - print("done!") - yield - - -app = FastAPI( - title="LightRAG API", description="API for RAG operations", lifespan=lifespan -) - -# Data models - - -class QueryRequest(BaseModel): - query: str - mode: str = "hybrid" - only_need_context: bool = False - - -class InsertRequest(BaseModel): - text: str - - -class Response(BaseModel): - status: str - data: Optional[str] = None - message: Optional[str] = None - - -# API routes - - -@app.post("/query", response_model=Response) -async def query_endpoint(request: QueryRequest): - try: - loop = asyncio.get_event_loop() - result = await loop.run_in_executor( - None, - lambda: rag.query( - request.query, - param=QueryParam( - mode=request.mode, only_need_context=request.only_need_context - ), - ), - ) - return Response(status="success", data=result) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post("/insert", response_model=Response) -async def insert_endpoint(request: InsertRequest): - try: - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: rag.insert(request.text)) - return Response(status="success", message="Text inserted successfully") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post("/insert_file", response_model=Response) -async def insert_file(file: UploadFile = File(...)): - try: - file_content = await file.read() - # Read file content - try: - content = file_content.decode("utf-8") - except UnicodeDecodeError: - # If UTF-8 decoding fails, try other encodings - content = file_content.decode("gbk") - # Insert file content - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: rag.insert(content)) - - return Response( - status="success", - message=f"File content from {file.filename} inserted successfully", - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/health") -async def health_check(): - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=8020) - -# Usage example -# To run the server, use the following command in your terminal: -# python lightrag_api_openai_compatible_demo.py - -# Example requests: -# 1. Query: -# curl -X POST "http://127.0.0.1:8020/query" -H "Content-Type: application/json" -d '{"query": "your query here", "mode": "hybrid"}' - -# 2. Insert text: -# curl -X POST "http://127.0.0.1:8020/insert" -H "Content-Type: application/json" -d '{"text": "your text here"}' - -# 3. Insert file: -# curl -X POST "http://127.0.0.1:8020/insert_file" -H "Content-Type: multipart/form-data" -F "file=@path/to/your/file.txt" - -# 4. Health check: -# curl -X GET "http://127.0.0.1:8020/health" diff --git a/examples/lightrag_api_oracle_demo.py b/examples/lightrag_api_oracle_demo.py deleted file mode 100644 index 3a82f479..00000000 --- a/examples/lightrag_api_oracle_demo.py +++ /dev/null @@ -1,267 +0,0 @@ -from fastapi import FastAPI, HTTPException, File, UploadFile -from fastapi import Query -from contextlib import asynccontextmanager -from pydantic import BaseModel -from typing import Optional, Any - -import sys -import os - - -from pathlib import Path - -import asyncio -import nest_asyncio -from lightrag import LightRAG, QueryParam -from lightrag.llm.openai import openai_complete_if_cache, openai_embed -from lightrag.utils import EmbeddingFunc -import numpy as np -from lightrag.kg.shared_storage import initialize_pipeline_status - - -print(os.getcwd()) -script_directory = Path(__file__).resolve().parent.parent -sys.path.append(os.path.abspath(script_directory)) - - -# Apply nest_asyncio to solve event loop issues -nest_asyncio.apply() - -DEFAULT_RAG_DIR = "index_default" - - -# We use OpenAI compatible API to call LLM on Oracle Cloud -# More docs here https://github.com/jin38324/OCI_GenAI_access_gateway -BASE_URL = "http://xxx.xxx.xxx.xxx:8088/v1/" -APIKEY = "ocigenerativeai" - -# Configure working directory -WORKING_DIR = os.environ.get("RAG_DIR", f"{DEFAULT_RAG_DIR}") -print(f"WORKING_DIR: {WORKING_DIR}") -LLM_MODEL = os.environ.get("LLM_MODEL", "cohere.command-r-plus-08-2024") -print(f"LLM_MODEL: {LLM_MODEL}") -EMBEDDING_MODEL = os.environ.get("EMBEDDING_MODEL", "cohere.embed-multilingual-v3.0") -print(f"EMBEDDING_MODEL: {EMBEDDING_MODEL}") -EMBEDDING_MAX_TOKEN_SIZE = int(os.environ.get("EMBEDDING_MAX_TOKEN_SIZE", 512)) -print(f"EMBEDDING_MAX_TOKEN_SIZE: {EMBEDDING_MAX_TOKEN_SIZE}") - -if not os.path.exists(WORKING_DIR): - os.mkdir(WORKING_DIR) - -os.environ["ORACLE_USER"] = "" -os.environ["ORACLE_PASSWORD"] = "" -os.environ["ORACLE_DSN"] = "" -os.environ["ORACLE_CONFIG_DIR"] = "path_to_config_dir" -os.environ["ORACLE_WALLET_LOCATION"] = "path_to_wallet_location" -os.environ["ORACLE_WALLET_PASSWORD"] = "wallet_password" -os.environ["ORACLE_WORKSPACE"] = "company" - - -async def llm_model_func( - prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs -) -> str: - return await openai_complete_if_cache( - LLM_MODEL, - prompt, - system_prompt=system_prompt, - history_messages=history_messages, - api_key=APIKEY, - base_url=BASE_URL, - **kwargs, - ) - - -async def embedding_func(texts: list[str]) -> np.ndarray: - return await openai_embed( - texts, - model=EMBEDDING_MODEL, - api_key=APIKEY, - base_url=BASE_URL, - ) - - -async def get_embedding_dim(): - test_text = ["This is a test sentence."] - embedding = await embedding_func(test_text) - embedding_dim = embedding.shape[1] - return embedding_dim - - -async def init(): - # Detect embedding dimension - embedding_dimension = await get_embedding_dim() - print(f"Detected embedding dimension: {embedding_dimension}") - # Create Oracle DB connection - # The `config` parameter is the connection configuration of Oracle DB - # More docs here https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html - # We storage data in unified tables, so we need to set a `workspace` parameter to specify which docs we want to store and query - # Below is an example of how to connect to Oracle Autonomous Database on Oracle Cloud - - # Initialize LightRAG - # We use Oracle DB as the KV/vector/graph storage - rag = LightRAG( - enable_llm_cache=False, - working_dir=WORKING_DIR, - chunk_token_size=512, - llm_model_func=llm_model_func, - embedding_func=EmbeddingFunc( - embedding_dim=embedding_dimension, - max_token_size=512, - func=embedding_func, - ), - graph_storage="OracleGraphStorage", - kv_storage="OracleKVStorage", - vector_storage="OracleVectorDBStorage", - ) - - await rag.initialize_storages() - await initialize_pipeline_status() - - return rag - - -# Extract and Insert into LightRAG storage -# with open("./dickens/book.txt", "r", encoding="utf-8") as f: -# await rag.ainsert(f.read()) - -# # Perform search in different modes -# modes = ["naive", "local", "global", "hybrid"] -# for mode in modes: -# print("="*20, mode, "="*20) -# print(await rag.aquery("这篇文档是关于什么内容的?", param=QueryParam(mode=mode))) -# print("-"*100, "\n") - -# Data models - - -class QueryRequest(BaseModel): - query: str - mode: str = "hybrid" - only_need_context: bool = False - only_need_prompt: bool = False - - -class DataRequest(BaseModel): - limit: int = 100 - - -class InsertRequest(BaseModel): - text: str - - -class Response(BaseModel): - status: str - data: Optional[Any] = None - message: Optional[str] = None - - -# API routes - -rag = None - - -@asynccontextmanager -async def lifespan(app: FastAPI): - global rag - rag = await init() - print("done!") - yield - - -app = FastAPI( - title="LightRAG API", description="API for RAG operations", lifespan=lifespan -) - - -@app.post("/query", response_model=Response) -async def query_endpoint(request: QueryRequest): - # try: - # loop = asyncio.get_event_loop() - if request.mode == "naive": - top_k = 3 - else: - top_k = 60 - result = await rag.aquery( - request.query, - param=QueryParam( - mode=request.mode, - only_need_context=request.only_need_context, - only_need_prompt=request.only_need_prompt, - top_k=top_k, - ), - ) - return Response(status="success", data=result) - # except Exception as e: - # raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/data", response_model=Response) -async def query_all_nodes(type: str = Query("nodes"), limit: int = Query(100)): - if type == "nodes": - result = await rag.chunk_entity_relation_graph.get_all_nodes(limit=limit) - elif type == "edges": - result = await rag.chunk_entity_relation_graph.get_all_edges(limit=limit) - elif type == "statistics": - result = await rag.chunk_entity_relation_graph.get_statistics() - return Response(status="success", data=result) - - -@app.post("/insert", response_model=Response) -async def insert_endpoint(request: InsertRequest): - try: - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: rag.insert(request.text)) - return Response(status="success", message="Text inserted successfully") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@app.post("/insert_file", response_model=Response) -async def insert_file(file: UploadFile = File(...)): - try: - file_content = await file.read() - # Read file content - try: - content = file_content.decode("utf-8") - except UnicodeDecodeError: - # If UTF-8 decoding fails, try other encodings - content = file_content.decode("gbk") - # Insert file content - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: rag.insert(content)) - - return Response( - status="success", - message=f"File content from {file.filename} inserted successfully", - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - -@app.get("/health") -async def health_check(): - return {"status": "healthy"} - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="127.0.0.1", port=8020) - -# Usage example -# To run the server, use the following command in your terminal: -# python lightrag_api_openai_compatible_demo.py - -# Example requests: -# 1. Query: -# curl -X POST "http://127.0.0.1:8020/query" -H "Content-Type: application/json" -d '{"query": "your query here", "mode": "hybrid"}' - -# 2. Insert text: -# curl -X POST "http://127.0.0.1:8020/insert" -H "Content-Type: application/json" -d '{"text": "your text here"}' - -# 3. Insert file: -# curl -X POST "http://127.0.0.1:8020/insert_file" -H "Content-Type: multipart/form-data" -F "file=@path/to/your/file.txt" - - -# 4. Health check: -# curl -X GET "http://127.0.0.1:8020/health" diff --git a/examples/lightrag_ollama_gremlin_demo.py b/examples/lightrag_ollama_gremlin_demo.py index 893b5606..7ae62810 100644 --- a/examples/lightrag_ollama_gremlin_demo.py +++ b/examples/lightrag_ollama_gremlin_demo.py @@ -1,3 +1,7 @@ +############################################## +# Gremlin storage implementation is deprecated +############################################## + import asyncio import inspect import os diff --git a/examples/lightrag_oracle_demo.py b/examples/lightrag_oracle_demo.py deleted file mode 100644 index 6663f6a1..00000000 --- a/examples/lightrag_oracle_demo.py +++ /dev/null @@ -1,141 +0,0 @@ -import sys -import os -from pathlib import Path -import asyncio -from lightrag import LightRAG, QueryParam -from lightrag.llm.openai import openai_complete_if_cache, openai_embed -from lightrag.utils import EmbeddingFunc -import numpy as np -from lightrag.kg.shared_storage import initialize_pipeline_status - -print(os.getcwd()) -script_directory = Path(__file__).resolve().parent.parent -sys.path.append(os.path.abspath(script_directory)) - -WORKING_DIR = "./dickens" - -# We use OpenAI compatible API to call LLM on Oracle Cloud -# More docs here https://github.com/jin38324/OCI_GenAI_access_gateway -BASE_URL = "http://xxx.xxx.xxx.xxx:8088/v1/" -APIKEY = "ocigenerativeai" -CHATMODEL = "cohere.command-r-plus" -EMBEDMODEL = "cohere.embed-multilingual-v3.0" -CHUNK_TOKEN_SIZE = 1024 -MAX_TOKENS = 4000 - -if not os.path.exists(WORKING_DIR): - os.mkdir(WORKING_DIR) - -os.environ["ORACLE_USER"] = "username" -os.environ["ORACLE_PASSWORD"] = "xxxxxxxxx" -os.environ["ORACLE_DSN"] = "xxxxxxx_medium" -os.environ["ORACLE_CONFIG_DIR"] = "path_to_config_dir" -os.environ["ORACLE_WALLET_LOCATION"] = "path_to_wallet_location" -os.environ["ORACLE_WALLET_PASSWORD"] = "wallet_password" -os.environ["ORACLE_WORKSPACE"] = "company" - - -async def llm_model_func( - prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs -) -> str: - return await openai_complete_if_cache( - CHATMODEL, - prompt, - system_prompt=system_prompt, - history_messages=history_messages, - api_key=APIKEY, - base_url=BASE_URL, - **kwargs, - ) - - -async def embedding_func(texts: list[str]) -> np.ndarray: - return await openai_embed( - texts, - model=EMBEDMODEL, - api_key=APIKEY, - base_url=BASE_URL, - ) - - -async def get_embedding_dim(): - test_text = ["This is a test sentence."] - embedding = await embedding_func(test_text) - embedding_dim = embedding.shape[1] - return embedding_dim - - -async def initialize_rag(): - # Detect embedding dimension - embedding_dimension = await get_embedding_dim() - print(f"Detected embedding dimension: {embedding_dimension}") - - # Initialize LightRAG - # We use Oracle DB as the KV/vector/graph storage - # You can add `addon_params={"example_number": 1, "language": "Simplfied Chinese"}` to control the prompt - rag = LightRAG( - # log_level="DEBUG", - working_dir=WORKING_DIR, - entity_extract_max_gleaning=1, - enable_llm_cache=True, - enable_llm_cache_for_entity_extract=True, - embedding_cache_config=None, # {"enabled": True,"similarity_threshold": 0.90}, - chunk_token_size=CHUNK_TOKEN_SIZE, - llm_model_max_token_size=MAX_TOKENS, - llm_model_func=llm_model_func, - embedding_func=EmbeddingFunc( - embedding_dim=embedding_dimension, - max_token_size=500, - func=embedding_func, - ), - graph_storage="OracleGraphStorage", - kv_storage="OracleKVStorage", - vector_storage="OracleVectorDBStorage", - addon_params={ - "example_number": 1, - "language": "Simplfied Chinese", - "entity_types": ["organization", "person", "geo", "event"], - "insert_batch_size": 2, - }, - ) - await rag.initialize_storages() - await initialize_pipeline_status() - - return rag - - -async def main(): - try: - # Initialize RAG instance - rag = await initialize_rag() - - # Extract and Insert into LightRAG storage - with open(WORKING_DIR + "/docs.txt", "r", encoding="utf-8") as f: - all_text = f.read() - texts = [x for x in all_text.split("\n") if x] - - # New mode use pipeline - await rag.apipeline_enqueue_documents(texts) - await rag.apipeline_process_enqueue_documents() - - # Old method use ainsert - # await rag.ainsert(texts) - - # Perform search in different modes - modes = ["naive", "local", "global", "hybrid"] - for mode in modes: - print("=" * 20, mode, "=" * 20) - print( - await rag.aquery( - "What are the top themes in this story?", - param=QueryParam(mode=mode), - ) - ) - print("-" * 100, "\n") - - except Exception as e: - print(f"An error occurred: {e}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/lightrag_tidb_demo.py b/examples/lightrag_tidb_demo.py index 52695560..50eac2ca 100644 --- a/examples/lightrag_tidb_demo.py +++ b/examples/lightrag_tidb_demo.py @@ -1,3 +1,7 @@ +########################################### +# TiDB storage implementation is deprecated +########################################### + import asyncio import os diff --git a/lightrag/api/README-zh.md b/lightrag/api/README-zh.md index 4bf31a61..03718656 100644 --- a/lightrag/api/README-zh.md +++ b/lightrag/api/README-zh.md @@ -291,11 +291,9 @@ LightRAG 使用 4 种类型的存储用于不同目的: ``` JsonKVStorage JsonFile(默认) -MongoKVStorage MogonDB -RedisKVStorage Redis -TiDBKVStorage TiDB PGKVStorage Postgres -OracleKVStorage Oracle +RedisKVStorage Redis +MongoKVStorage MogonDB ``` * GRAPH_STORAGE 支持的实现名称 @@ -303,25 +301,19 @@ OracleKVStorage Oracle ``` NetworkXStorage NetworkX(默认) Neo4JStorage Neo4J -MongoGraphStorage MongoDB -TiDBGraphStorage TiDB -AGEStorage AGE -GremlinStorage Gremlin PGGraphStorage Postgres -OracleGraphStorage Postgres +AGEStorage AGE ``` * VECTOR_STORAGE 支持的实现名称 ``` NanoVectorDBStorage NanoVector(默认) +PGVectorStorage Postgres MilvusVectorDBStorge Milvus ChromaVectorDBStorage Chroma -TiDBVectorDBStorage TiDB -PGVectorStorage Postgres FaissVectorDBStorage Faiss QdrantVectorDBStorage Qdrant -OracleVectorDBStorage Oracle MongoVectorDBStorage MongoDB ``` diff --git a/lightrag/api/README.md b/lightrag/api/README.md index 8b2e8177..27f3d14a 100644 --- a/lightrag/api/README.md +++ b/lightrag/api/README.md @@ -302,11 +302,9 @@ Each storage type have servals implementations: ``` JsonKVStorage JsonFile(default) -MongoKVStorage MogonDB -RedisKVStorage Redis -TiDBKVStorage TiDB PGKVStorage Postgres -OracleKVStorage Oracle +RedisKVStorage Redis +MongoKVStorage MogonDB ``` * GRAPH_STORAGE supported implement-name @@ -314,25 +312,19 @@ OracleKVStorage Oracle ``` NetworkXStorage NetworkX(defualt) Neo4JStorage Neo4J -MongoGraphStorage MongoDB -TiDBGraphStorage TiDB -AGEStorage AGE -GremlinStorage Gremlin PGGraphStorage Postgres -OracleGraphStorage Postgres +AGEStorage AGE ``` * VECTOR_STORAGE supported implement-name ``` NanoVectorDBStorage NanoVector(default) -MilvusVectorDBStorage Milvus -ChromaVectorDBStorage Chroma -TiDBVectorDBStorage TiDB PGVectorStorage Postgres +MilvusVectorDBStorge Milvus +ChromaVectorDBStorage Chroma FaissVectorDBStorage Faiss QdrantVectorDBStorage Qdrant -OracleVectorDBStorage Oracle MongoVectorDBStorage MongoDB ``` diff --git a/lightrag/api/__init__.py b/lightrag/api/__init__.py index ec1959de..75eb6b64 100644 --- a/lightrag/api/__init__.py +++ b/lightrag/api/__init__.py @@ -1 +1 @@ -__api_version__ = "1.2.8" +__api_version__ = "0136" diff --git a/lightrag/api/auth.py b/lightrag/api/auth.py index 58175b9d..0b61095d 100644 --- a/lightrag/api/auth.py +++ b/lightrag/api/auth.py @@ -1,9 +1,11 @@ -import os from datetime import datetime, timedelta + import jwt +from dotenv import load_dotenv from fastapi import HTTPException, status from pydantic import BaseModel -from dotenv import load_dotenv + +from .config import global_args # use the .env that is inside the current folder # allows to use different .env file for each lightrag instance @@ -20,13 +22,12 @@ class TokenPayload(BaseModel): class AuthHandler: def __init__(self): - self.secret = os.getenv("TOKEN_SECRET", "4f85ds4f56dsf46") - self.algorithm = "HS256" - self.expire_hours = int(os.getenv("TOKEN_EXPIRE_HOURS", 4)) - self.guest_expire_hours = int(os.getenv("GUEST_TOKEN_EXPIRE_HOURS", 2)) - + self.secret = global_args.token_secret + self.algorithm = global_args.jwt_algorithm + self.expire_hours = global_args.token_expire_hours + self.guest_expire_hours = global_args.guest_token_expire_hours self.accounts = {} - auth_accounts = os.getenv("AUTH_ACCOUNTS") + auth_accounts = global_args.auth_accounts if auth_accounts: for account in auth_accounts.split(","): username, password = account.split(":", 1) diff --git a/lightrag/api/config.py b/lightrag/api/config.py new file mode 100644 index 00000000..1bbdb1c9 --- /dev/null +++ b/lightrag/api/config.py @@ -0,0 +1,335 @@ +""" +Configs for the LightRAG API. +""" + +import os +import argparse +import logging +from dotenv import load_dotenv + +# use the .env that is inside the current folder +# allows to use different .env file for each lightrag instance +# the OS environment variables take precedence over the .env file +load_dotenv(dotenv_path=".env", override=False) + + +class OllamaServerInfos: + # Constants for emulated Ollama model information + LIGHTRAG_NAME = "lightrag" + LIGHTRAG_TAG = os.getenv("OLLAMA_EMULATING_MODEL_TAG", "latest") + LIGHTRAG_MODEL = f"{LIGHTRAG_NAME}:{LIGHTRAG_TAG}" + LIGHTRAG_SIZE = 7365960935 # it's a dummy value + LIGHTRAG_CREATED_AT = "2024-01-15T00:00:00Z" + LIGHTRAG_DIGEST = "sha256:lightrag" + + +ollama_server_infos = OllamaServerInfos() + + +class DefaultRAGStorageConfig: + KV_STORAGE = "JsonKVStorage" + VECTOR_STORAGE = "NanoVectorDBStorage" + GRAPH_STORAGE = "NetworkXStorage" + DOC_STATUS_STORAGE = "JsonDocStatusStorage" + + +def get_default_host(binding_type: str) -> str: + default_hosts = { + "ollama": os.getenv("LLM_BINDING_HOST", "http://localhost:11434"), + "lollms": os.getenv("LLM_BINDING_HOST", "http://localhost:9600"), + "azure_openai": os.getenv("AZURE_OPENAI_ENDPOINT", "https://api.openai.com/v1"), + "openai": os.getenv("LLM_BINDING_HOST", "https://api.openai.com/v1"), + } + return default_hosts.get( + binding_type, os.getenv("LLM_BINDING_HOST", "http://localhost:11434") + ) # fallback to ollama if unknown + + +def get_env_value(env_key: str, default: any, value_type: type = str) -> any: + """ + Get value from environment variable with type conversion + + Args: + env_key (str): Environment variable key + default (any): Default value if env variable is not set + value_type (type): Type to convert the value to + + Returns: + any: Converted value from environment or default + """ + value = os.getenv(env_key) + if value is None: + return default + + if value_type is bool: + return value.lower() in ("true", "1", "yes", "t", "on") + try: + return value_type(value) + except ValueError: + return default + + +def parse_args() -> argparse.Namespace: + """ + Parse command line arguments with environment variable fallback + + Args: + is_uvicorn_mode: Whether running under uvicorn mode + + Returns: + argparse.Namespace: Parsed arguments + """ + + parser = argparse.ArgumentParser( + description="LightRAG FastAPI Server with separate working and input directories" + ) + + # Server configuration + parser.add_argument( + "--host", + default=get_env_value("HOST", "0.0.0.0"), + help="Server host (default: from env or 0.0.0.0)", + ) + parser.add_argument( + "--port", + type=int, + default=get_env_value("PORT", 9621, int), + help="Server port (default: from env or 9621)", + ) + + # Directory configuration + parser.add_argument( + "--working-dir", + default=get_env_value("WORKING_DIR", "./rag_storage"), + help="Working directory for RAG storage (default: from env or ./rag_storage)", + ) + parser.add_argument( + "--input-dir", + default=get_env_value("INPUT_DIR", "./inputs"), + help="Directory containing input documents (default: from env or ./inputs)", + ) + + def timeout_type(value): + if value is None: + return 150 + if value is None or value == "None": + return None + return int(value) + + parser.add_argument( + "--timeout", + default=get_env_value("TIMEOUT", None, timeout_type), + type=timeout_type, + help="Timeout in seconds (useful when using slow AI). Use None for infinite timeout", + ) + + # RAG configuration + parser.add_argument( + "--max-async", + type=int, + default=get_env_value("MAX_ASYNC", 4, int), + help="Maximum async operations (default: from env or 4)", + ) + parser.add_argument( + "--max-tokens", + type=int, + default=get_env_value("MAX_TOKENS", 32768, int), + help="Maximum token size (default: from env or 32768)", + ) + + # Logging configuration + parser.add_argument( + "--log-level", + default=get_env_value("LOG_LEVEL", "INFO"), + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Logging level (default: from env or INFO)", + ) + parser.add_argument( + "--verbose", + action="store_true", + default=get_env_value("VERBOSE", False, bool), + help="Enable verbose debug output(only valid for DEBUG log-level)", + ) + + parser.add_argument( + "--key", + type=str, + default=get_env_value("LIGHTRAG_API_KEY", None), + help="API key for authentication. This protects lightrag server against unauthorized access", + ) + + # Optional https parameters + parser.add_argument( + "--ssl", + action="store_true", + default=get_env_value("SSL", False, bool), + help="Enable HTTPS (default: from env or False)", + ) + parser.add_argument( + "--ssl-certfile", + default=get_env_value("SSL_CERTFILE", None), + help="Path to SSL certificate file (required if --ssl is enabled)", + ) + parser.add_argument( + "--ssl-keyfile", + default=get_env_value("SSL_KEYFILE", None), + help="Path to SSL private key file (required if --ssl is enabled)", + ) + + parser.add_argument( + "--history-turns", + type=int, + default=get_env_value("HISTORY_TURNS", 3, int), + help="Number of conversation history turns to include (default: from env or 3)", + ) + + # Search parameters + parser.add_argument( + "--top-k", + type=int, + default=get_env_value("TOP_K", 60, int), + help="Number of most similar results to return (default: from env or 60)", + ) + parser.add_argument( + "--cosine-threshold", + type=float, + default=get_env_value("COSINE_THRESHOLD", 0.2, float), + help="Cosine similarity threshold (default: from env or 0.4)", + ) + + # Ollama model name + parser.add_argument( + "--simulated-model-name", + type=str, + default=get_env_value( + "SIMULATED_MODEL_NAME", ollama_server_infos.LIGHTRAG_MODEL + ), + help="Number of conversation history turns to include (default: from env or 3)", + ) + + # Namespace + parser.add_argument( + "--namespace-prefix", + type=str, + default=get_env_value("NAMESPACE_PREFIX", ""), + help="Prefix of the namespace", + ) + + parser.add_argument( + "--auto-scan-at-startup", + action="store_true", + default=False, + help="Enable automatic scanning when the program starts", + ) + + # Server workers configuration + parser.add_argument( + "--workers", + type=int, + default=get_env_value("WORKERS", 1, int), + help="Number of worker processes (default: from env or 1)", + ) + + # LLM and embedding bindings + parser.add_argument( + "--llm-binding", + type=str, + default=get_env_value("LLM_BINDING", "ollama"), + choices=["lollms", "ollama", "openai", "openai-ollama", "azure_openai"], + help="LLM binding type (default: from env or ollama)", + ) + parser.add_argument( + "--embedding-binding", + type=str, + default=get_env_value("EMBEDDING_BINDING", "ollama"), + choices=["lollms", "ollama", "openai", "azure_openai"], + help="Embedding binding type (default: from env or ollama)", + ) + + args = parser.parse_args() + + # convert relative path to absolute path + args.working_dir = os.path.abspath(args.working_dir) + args.input_dir = os.path.abspath(args.input_dir) + + # Inject storage configuration from environment variables + args.kv_storage = get_env_value( + "LIGHTRAG_KV_STORAGE", DefaultRAGStorageConfig.KV_STORAGE + ) + args.doc_status_storage = get_env_value( + "LIGHTRAG_DOC_STATUS_STORAGE", DefaultRAGStorageConfig.DOC_STATUS_STORAGE + ) + args.graph_storage = get_env_value( + "LIGHTRAG_GRAPH_STORAGE", DefaultRAGStorageConfig.GRAPH_STORAGE + ) + args.vector_storage = get_env_value( + "LIGHTRAG_VECTOR_STORAGE", DefaultRAGStorageConfig.VECTOR_STORAGE + ) + + # Get MAX_PARALLEL_INSERT from environment + args.max_parallel_insert = get_env_value("MAX_PARALLEL_INSERT", 2, int) + + # Handle openai-ollama special case + if args.llm_binding == "openai-ollama": + args.llm_binding = "openai" + args.embedding_binding = "ollama" + + args.llm_binding_host = get_env_value( + "LLM_BINDING_HOST", get_default_host(args.llm_binding) + ) + args.embedding_binding_host = get_env_value( + "EMBEDDING_BINDING_HOST", get_default_host(args.embedding_binding) + ) + args.llm_binding_api_key = get_env_value("LLM_BINDING_API_KEY", None) + args.embedding_binding_api_key = get_env_value("EMBEDDING_BINDING_API_KEY", "") + + # Inject model configuration + args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest") + args.embedding_model = get_env_value("EMBEDDING_MODEL", "bge-m3:latest") + args.embedding_dim = get_env_value("EMBEDDING_DIM", 1024, int) + args.max_embed_tokens = get_env_value("MAX_EMBED_TOKENS", 8192, int) + + # Inject chunk configuration + args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int) + args.chunk_overlap_size = get_env_value("CHUNK_OVERLAP_SIZE", 100, int) + + # Inject LLM cache configuration + args.enable_llm_cache_for_extract = get_env_value( + "ENABLE_LLM_CACHE_FOR_EXTRACT", True, bool + ) + + # Inject LLM temperature configuration + args.temperature = get_env_value("TEMPERATURE", 0.5, float) + + # Select Document loading tool (DOCLING, DEFAULT) + args.document_loading_engine = get_env_value("DOCUMENT_LOADING_ENGINE", "DEFAULT") + + # Add environment variables that were previously read directly + args.cors_origins = get_env_value("CORS_ORIGINS", "*") + args.summary_language = get_env_value("SUMMARY_LANGUAGE", "en") + args.whitelist_paths = get_env_value("WHITELIST_PATHS", "/health,/api/*") + + # For JWT Auth + args.auth_accounts = get_env_value("AUTH_ACCOUNTS", "") + args.token_secret = get_env_value("TOKEN_SECRET", "lightrag-jwt-default-secret") + args.token_expire_hours = get_env_value("TOKEN_EXPIRE_HOURS", 48, int) + args.guest_token_expire_hours = get_env_value("GUEST_TOKEN_EXPIRE_HOURS", 24, int) + args.jwt_algorithm = get_env_value("JWT_ALGORITHM", "HS256") + + ollama_server_infos.LIGHTRAG_MODEL = args.simulated_model_name + + return args + + +def update_uvicorn_mode_config(): + # If in uvicorn mode and workers > 1, force it to 1 and log warning + if global_args.workers > 1: + original_workers = global_args.workers + global_args.workers = 1 + # Log warning directly here + logging.warning( + f"In uvicorn mode, workers parameter was set to {original_workers}. Forcing workers=1" + ) + + +global_args = parse_args() diff --git a/lightrag/api/lightrag_server.py b/lightrag/api/lightrag_server.py index 8110d6d4..84636bde 100644 --- a/lightrag/api/lightrag_server.py +++ b/lightrag/api/lightrag_server.py @@ -19,11 +19,14 @@ from contextlib import asynccontextmanager from dotenv import load_dotenv from lightrag.api.utils_api import ( get_combined_auth_dependency, - parse_args, - get_default_host, display_splash_screen, check_env_file, ) +from .config import ( + global_args, + update_uvicorn_mode_config, + get_default_host, +) import sys from lightrag import LightRAG, __version__ as core_version from lightrag.api import __api_version__ @@ -52,6 +55,10 @@ from lightrag.api.auth import auth_handler # the OS environment variables take precedence over the .env file load_dotenv(dotenv_path=".env", override=False) + +webui_title = os.getenv("WEBUI_TITLE") +webui_description = os.getenv("WEBUI_DESCRIPTION") + # Initialize config parser config = configparser.ConfigParser() config.read("config.ini") @@ -164,10 +171,10 @@ def create_app(args): app = FastAPI(**app_kwargs) def get_cors_origins(): - """Get allowed origins from environment variable + """Get allowed origins from global_args Returns a list of allowed origins, defaults to ["*"] if not set """ - origins_str = os.getenv("CORS_ORIGINS", "*") + origins_str = global_args.cors_origins if origins_str == "*": return ["*"] return [origin.strip() for origin in origins_str.split(",")] @@ -315,9 +322,10 @@ def create_app(args): "similarity_threshold": 0.95, "use_llm_check": False, }, - namespace_prefix=args.namespace_prefix, + # namespace_prefix=args.namespace_prefix, auto_manage_storages_states=False, max_parallel_insert=args.max_parallel_insert, + addon_params={"language": args.summary_language}, ) else: # azure_openai rag = LightRAG( @@ -345,9 +353,10 @@ def create_app(args): "similarity_threshold": 0.95, "use_llm_check": False, }, - namespace_prefix=args.namespace_prefix, + # namespace_prefix=args.namespace_prefix, auto_manage_storages_states=False, max_parallel_insert=args.max_parallel_insert, + addon_params={"language": args.summary_language}, ) # Add routes @@ -381,6 +390,8 @@ def create_app(args): "message": "Authentication is disabled. Using guest access.", "core_version": core_version, "api_version": __api_version__, + "webui_title": webui_title, + "webui_description": webui_description, } return { @@ -388,6 +399,8 @@ def create_app(args): "auth_mode": "enabled", "core_version": core_version, "api_version": __api_version__, + "webui_title": webui_title, + "webui_description": webui_description, } @app.post("/login") @@ -404,6 +417,8 @@ def create_app(args): "message": "Authentication is disabled. Using guest access.", "core_version": core_version, "api_version": __api_version__, + "webui_title": webui_title, + "webui_description": webui_description, } username = form_data.username if auth_handler.accounts.get(username) != form_data.password: @@ -421,6 +436,8 @@ def create_app(args): "auth_mode": "enabled", "core_version": core_version, "api_version": __api_version__, + "webui_title": webui_title, + "webui_description": webui_description, } @app.get("/health", dependencies=[Depends(combined_auth)]) @@ -454,10 +471,12 @@ def create_app(args): "vector_storage": args.vector_storage, "enable_llm_cache_for_extract": args.enable_llm_cache_for_extract, }, - "core_version": core_version, - "api_version": __api_version__, "auth_mode": auth_mode, "pipeline_busy": pipeline_status.get("busy", False), + "core_version": core_version, + "api_version": __api_version__, + "webui_title": webui_title, + "webui_description": webui_description, } except Exception as e: logger.error(f"Error getting health status: {str(e)}") @@ -490,7 +509,7 @@ def create_app(args): def get_application(args=None): """Factory function for creating the FastAPI application""" if args is None: - args = parse_args() + args = global_args return create_app(args) @@ -611,30 +630,31 @@ def main(): # Configure logging before parsing args configure_logging() - - args = parse_args(is_uvicorn_mode=True) - display_splash_screen(args) + update_uvicorn_mode_config() + display_splash_screen(global_args) # Create application instance directly instead of using factory function - app = create_app(args) + app = create_app(global_args) # Start Uvicorn in single process mode uvicorn_config = { "app": app, # Pass application instance directly instead of string path - "host": args.host, - "port": args.port, + "host": global_args.host, + "port": global_args.port, "log_config": None, # Disable default config } - if args.ssl: + if global_args.ssl: uvicorn_config.update( { - "ssl_certfile": args.ssl_certfile, - "ssl_keyfile": args.ssl_keyfile, + "ssl_certfile": global_args.ssl_certfile, + "ssl_keyfile": global_args.ssl_keyfile, } ) - print(f"Starting Uvicorn server in single-process mode on {args.host}:{args.port}") + print( + f"Starting Uvicorn server in single-process mode on {global_args.host}:{global_args.port}" + ) uvicorn.run(**uvicorn_config) diff --git a/lightrag/api/routers/document_routes.py b/lightrag/api/routers/document_routes.py index 445008ec..8e664006 100644 --- a/lightrag/api/routers/document_routes.py +++ b/lightrag/api/routers/document_routes.py @@ -10,16 +10,14 @@ import traceback import pipmaster as pm from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional, Any +from typing import Dict, List, Optional, Any, Literal from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, UploadFile from pydantic import BaseModel, Field, field_validator from lightrag import LightRAG from lightrag.base import DocProcessingStatus, DocStatus -from lightrag.api.utils_api import ( - get_combined_auth_dependency, - global_args, -) +from lightrag.api.utils_api import get_combined_auth_dependency +from ..config import global_args router = APIRouter( prefix="/documents", @@ -30,7 +28,37 @@ router = APIRouter( temp_prefix = "__tmp__" +class ScanResponse(BaseModel): + """Response model for document scanning operation + + Attributes: + status: Status of the scanning operation + message: Optional message with additional details + """ + + status: Literal["scanning_started"] = Field( + description="Status of the scanning operation" + ) + message: Optional[str] = Field( + default=None, description="Additional details about the scanning operation" + ) + + class Config: + json_schema_extra = { + "example": { + "status": "scanning_started", + "message": "Scanning process has been initiated in the background", + } + } + + class InsertTextRequest(BaseModel): + """Request model for inserting a single text document + + Attributes: + text: The text content to be inserted into the RAG system + """ + text: str = Field( min_length=1, description="The text to insert", @@ -41,8 +69,21 @@ class InsertTextRequest(BaseModel): def strip_after(cls, text: str) -> str: return text.strip() + class Config: + json_schema_extra = { + "example": { + "text": "This is a sample text to be inserted into the RAG system." + } + } + class InsertTextsRequest(BaseModel): + """Request model for inserting multiple text documents + + Attributes: + texts: List of text contents to be inserted into the RAG system + """ + texts: list[str] = Field( min_length=1, description="The texts to insert", @@ -53,11 +94,116 @@ class InsertTextsRequest(BaseModel): def strip_after(cls, texts: list[str]) -> list[str]: return [text.strip() for text in texts] + class Config: + json_schema_extra = { + "example": { + "texts": [ + "This is the first text to be inserted.", + "This is the second text to be inserted.", + ] + } + } + class InsertResponse(BaseModel): - status: str = Field(description="Status of the operation") + """Response model for document insertion operations + + Attributes: + status: Status of the operation (success, duplicated, partial_success, failure) + message: Detailed message describing the operation result + """ + + status: Literal["success", "duplicated", "partial_success", "failure"] = Field( + description="Status of the operation" + ) message: str = Field(description="Message describing the operation result") + class Config: + json_schema_extra = { + "example": { + "status": "success", + "message": "File 'document.pdf' uploaded successfully. Processing will continue in background.", + } + } + + +class ClearDocumentsResponse(BaseModel): + """Response model for document clearing operation + + Attributes: + status: Status of the clear operation + message: Detailed message describing the operation result + """ + + status: Literal["success", "partial_success", "busy", "fail"] = Field( + description="Status of the clear operation" + ) + message: str = Field(description="Message describing the operation result") + + class Config: + json_schema_extra = { + "example": { + "status": "success", + "message": "All documents cleared successfully. Deleted 15 files.", + } + } + + +class ClearCacheRequest(BaseModel): + """Request model for clearing cache + + Attributes: + modes: Optional list of cache modes to clear + """ + + modes: Optional[ + List[Literal["default", "naive", "local", "global", "hybrid", "mix"]] + ] = Field( + default=None, + description="Modes of cache to clear. If None, clears all cache.", + ) + + class Config: + json_schema_extra = {"example": {"modes": ["default", "naive"]}} + + +class ClearCacheResponse(BaseModel): + """Response model for cache clearing operation + + Attributes: + status: Status of the clear operation + message: Detailed message describing the operation result + """ + + status: Literal["success", "fail"] = Field( + description="Status of the clear operation" + ) + message: str = Field(description="Message describing the operation result") + + class Config: + json_schema_extra = { + "example": { + "status": "success", + "message": "Successfully cleared cache for modes: ['default', 'naive']", + } + } + + +"""Response model for document status + +Attributes: + id: Document identifier + content_summary: Summary of document content + content_length: Length of document content + status: Current processing status + created_at: Creation timestamp (ISO format string) + updated_at: Last update timestamp (ISO format string) + chunks_count: Number of chunks (optional) + error: Error message if any (optional) + metadata: Additional metadata (optional) + file_path: Path to the document file +""" + class DocStatusResponse(BaseModel): @staticmethod @@ -68,34 +214,82 @@ class DocStatusResponse(BaseModel): return dt return dt.isoformat() - """Response model for document status + id: str = Field(description="Document identifier") + content_summary: str = Field(description="Summary of document content") + content_length: int = Field(description="Length of document content in characters") + status: DocStatus = Field(description="Current processing status") + created_at: str = Field(description="Creation timestamp (ISO format string)") + updated_at: str = Field(description="Last update timestamp (ISO format string)") + chunks_count: Optional[int] = Field( + default=None, description="Number of chunks the document was split into" + ) + error: Optional[str] = Field( + default=None, description="Error message if processing failed" + ) + metadata: Optional[dict[str, Any]] = Field( + default=None, description="Additional metadata about the document" + ) + file_path: str = Field(description="Path to the document file") - Attributes: - id: Document identifier - content_summary: Summary of document content - content_length: Length of document content - status: Current processing status - created_at: Creation timestamp (ISO format string) - updated_at: Last update timestamp (ISO format string) - chunks_count: Number of chunks (optional) - error: Error message if any (optional) - metadata: Additional metadata (optional) - """ - - 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 - file_path: str + class Config: + json_schema_extra = { + "example": { + "id": "doc_123456", + "content_summary": "Research paper on machine learning", + "content_length": 15240, + "status": "PROCESSED", + "created_at": "2025-03-31T12:34:56", + "updated_at": "2025-03-31T12:35:30", + "chunks_count": 12, + "error": None, + "metadata": {"author": "John Doe", "year": 2025}, + "file_path": "research_paper.pdf", + } + } class DocsStatusesResponse(BaseModel): - statuses: Dict[DocStatus, List[DocStatusResponse]] = {} + """Response model for document statuses + + Attributes: + statuses: Dictionary mapping document status to lists of document status responses + """ + + statuses: Dict[DocStatus, List[DocStatusResponse]] = Field( + default_factory=dict, + description="Dictionary mapping document status to lists of document status responses", + ) + + class Config: + json_schema_extra = { + "example": { + "statuses": { + "PENDING": [ + { + "id": "doc_123", + "content_summary": "Pending document", + "content_length": 5000, + "status": "PENDING", + "created_at": "2025-03-31T10:00:00", + "updated_at": "2025-03-31T10:00:00", + "file_path": "pending_doc.pdf", + } + ], + "PROCESSED": [ + { + "id": "doc_456", + "content_summary": "Processed document", + "content_length": 8000, + "status": "PROCESSED", + "created_at": "2025-03-31T09:00:00", + "updated_at": "2025-03-31T09:05:00", + "chunks_count": 8, + "file_path": "processed_doc.pdf", + } + ], + } + } + } class PipelineStatusResponse(BaseModel): @@ -276,7 +470,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool: ) return False case ".pdf": - if global_args["main_args"].document_loading_engine == "DOCLING": + if global_args.document_loading_engine == "DOCLING": if not pm.is_installed("docling"): # type: ignore pm.install("docling") from docling.document_converter import DocumentConverter # type: ignore @@ -295,7 +489,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool: for page in reader.pages: content += page.extract_text() + "\n" case ".docx": - if global_args["main_args"].document_loading_engine == "DOCLING": + if global_args.document_loading_engine == "DOCLING": if not pm.is_installed("docling"): # type: ignore pm.install("docling") from docling.document_converter import DocumentConverter # type: ignore @@ -315,7 +509,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool: [paragraph.text for paragraph in doc.paragraphs] ) case ".pptx": - if global_args["main_args"].document_loading_engine == "DOCLING": + if global_args.document_loading_engine == "DOCLING": if not pm.is_installed("docling"): # type: ignore pm.install("docling") from docling.document_converter import DocumentConverter # type: ignore @@ -336,7 +530,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool: if hasattr(shape, "text"): content += shape.text + "\n" case ".xlsx": - if global_args["main_args"].document_loading_engine == "DOCLING": + if global_args.document_loading_engine == "DOCLING": if not pm.is_installed("docling"): # type: ignore pm.install("docling") from docling.document_converter import DocumentConverter # type: ignore @@ -443,6 +637,7 @@ async def pipeline_index_texts(rag: LightRAG, texts: List[str]): await rag.apipeline_process_enqueue_documents() +# TODO: deprecate after /insert_file is removed async def save_temp_file(input_dir: Path, file: UploadFile = File(...)) -> Path: """Save the uploaded file to a temporary location @@ -476,8 +671,8 @@ async def run_scanning_process(rag: LightRAG, doc_manager: DocumentManager): if not new_files: return - # Get MAX_PARALLEL_INSERT from global_args["main_args"] - max_parallel = global_args["main_args"].max_parallel_insert + # Get MAX_PARALLEL_INSERT from global_args + max_parallel = global_args.max_parallel_insert # Calculate batch size as 2 * MAX_PARALLEL_INSERT batch_size = 2 * max_parallel @@ -509,7 +704,9 @@ def create_document_routes( # Create combined auth dependency for document routes combined_auth = get_combined_auth_dependency(api_key) - @router.post("/scan", dependencies=[Depends(combined_auth)]) + @router.post( + "/scan", response_model=ScanResponse, dependencies=[Depends(combined_auth)] + ) async def scan_for_new_documents(background_tasks: BackgroundTasks): """ Trigger the scanning process for new documents. @@ -519,13 +716,18 @@ def create_document_routes( that fact. Returns: - dict: A dictionary containing the scanning status + ScanResponse: A response object containing the scanning status """ # Start the scanning process in the background background_tasks.add_task(run_scanning_process, rag, doc_manager) - return {"status": "scanning_started"} + return ScanResponse( + status="scanning_started", + message="Scanning process has been initiated in the background", + ) - @router.post("/upload", dependencies=[Depends(combined_auth)]) + @router.post( + "/upload", response_model=InsertResponse, dependencies=[Depends(combined_auth)] + ) async def upload_to_input_dir( background_tasks: BackgroundTasks, file: UploadFile = File(...) ): @@ -645,6 +847,7 @@ def create_document_routes( logger.error(traceback.format_exc()) raise HTTPException(status_code=500, detail=str(e)) + # TODO: deprecated, use /upload instead @router.post( "/file", response_model=InsertResponse, dependencies=[Depends(combined_auth)] ) @@ -688,6 +891,7 @@ def create_document_routes( logger.error(traceback.format_exc()) raise HTTPException(status_code=500, detail=str(e)) + # TODO: deprecated, use /upload instead @router.post( "/file_batch", response_model=InsertResponse, @@ -752,32 +956,186 @@ def create_document_routes( raise HTTPException(status_code=500, detail=str(e)) @router.delete( - "", response_model=InsertResponse, dependencies=[Depends(combined_auth)] + "", response_model=ClearDocumentsResponse, dependencies=[Depends(combined_auth)] ) async def clear_documents(): """ Clear all documents from the RAG system. - This endpoint deletes all text chunks, entities vector database, and relationships - vector database, effectively clearing all documents from the RAG system. + This endpoint deletes all documents, entities, relationships, and files from the system. + It uses the storage drop methods to properly clean up all data and removes all files + from the input directory. Returns: - InsertResponse: A response object containing the status and message. + ClearDocumentsResponse: A response object containing the status and message. + - status="success": All documents and files were successfully cleared. + - status="partial_success": Document clear job exit with some errors. + - status="busy": Operation could not be completed because the pipeline is busy. + - status="fail": All storage drop operations failed, with message + - message: Detailed information about the operation results, including counts + of deleted files and any errors encountered. Raises: - HTTPException: If an error occurs during the clearing process (500). + HTTPException: Raised when a serious error occurs during the clearing process, + with status code 500 and error details in the detail field. """ - try: - rag.text_chunks = [] - rag.entities_vdb = None - rag.relationships_vdb = None - return InsertResponse( - status="success", message="All documents cleared successfully" + from lightrag.kg.shared_storage import ( + get_namespace_data, + get_pipeline_status_lock, + ) + + # Get pipeline status and lock + pipeline_status = await get_namespace_data("pipeline_status") + pipeline_status_lock = get_pipeline_status_lock() + + # Check and set status with lock + async with pipeline_status_lock: + if pipeline_status.get("busy", False): + return ClearDocumentsResponse( + status="busy", + message="Cannot clear documents while pipeline is busy", + ) + # Set busy to true + pipeline_status.update( + { + "busy": True, + "job_name": "Clearing Documents", + "job_start": datetime.now().isoformat(), + "docs": 0, + "batchs": 0, + "cur_batch": 0, + "request_pending": False, # Clear any previous request + "latest_message": "Starting document clearing process", + } ) + # Cleaning history_messages without breaking it as a shared list object + del pipeline_status["history_messages"][:] + pipeline_status["history_messages"].append( + "Starting document clearing process" + ) + + try: + # Use drop method to clear all data + drop_tasks = [] + storages = [ + rag.text_chunks, + rag.full_docs, + rag.entities_vdb, + rag.relationships_vdb, + rag.chunks_vdb, + rag.chunk_entity_relation_graph, + rag.doc_status, + ] + + # Log storage drop start + if "history_messages" in pipeline_status: + pipeline_status["history_messages"].append( + "Starting to drop storage components" + ) + + for storage in storages: + if storage is not None: + drop_tasks.append(storage.drop()) + + # Wait for all drop tasks to complete + drop_results = await asyncio.gather(*drop_tasks, return_exceptions=True) + + # Check for errors and log results + errors = [] + storage_success_count = 0 + storage_error_count = 0 + + for i, result in enumerate(drop_results): + storage_name = storages[i].__class__.__name__ + if isinstance(result, Exception): + error_msg = f"Error dropping {storage_name}: {str(result)}" + errors.append(error_msg) + logger.error(error_msg) + storage_error_count += 1 + else: + logger.info(f"Successfully dropped {storage_name}") + storage_success_count += 1 + + # Log storage drop results + if "history_messages" in pipeline_status: + if storage_error_count > 0: + pipeline_status["history_messages"].append( + f"Dropped {storage_success_count} storage components with {storage_error_count} errors" + ) + else: + pipeline_status["history_messages"].append( + f"Successfully dropped all {storage_success_count} storage components" + ) + + # If all storage operations failed, return error status and don't proceed with file deletion + if storage_success_count == 0 and storage_error_count > 0: + error_message = "All storage drop operations failed. Aborting document clearing process." + logger.error(error_message) + if "history_messages" in pipeline_status: + pipeline_status["history_messages"].append(error_message) + return ClearDocumentsResponse(status="fail", message=error_message) + + # Log file deletion start + if "history_messages" in pipeline_status: + pipeline_status["history_messages"].append( + "Starting to delete files in input directory" + ) + + # Delete all files in input_dir + deleted_files_count = 0 + file_errors_count = 0 + + for file_path in doc_manager.input_dir.glob("**/*"): + if file_path.is_file(): + try: + file_path.unlink() + deleted_files_count += 1 + except Exception as e: + logger.error(f"Error deleting file {file_path}: {str(e)}") + file_errors_count += 1 + + # Log file deletion results + if "history_messages" in pipeline_status: + if file_errors_count > 0: + pipeline_status["history_messages"].append( + f"Deleted {deleted_files_count} files with {file_errors_count} errors" + ) + errors.append(f"Failed to delete {file_errors_count} files") + else: + pipeline_status["history_messages"].append( + f"Successfully deleted {deleted_files_count} files" + ) + + # Prepare final result message + final_message = "" + if errors: + final_message = f"Cleared documents with some errors. Deleted {deleted_files_count} files." + status = "partial_success" + else: + final_message = f"All documents cleared successfully. Deleted {deleted_files_count} files." + status = "success" + + # Log final result + if "history_messages" in pipeline_status: + pipeline_status["history_messages"].append(final_message) + + # Return response based on results + return ClearDocumentsResponse(status=status, message=final_message) except Exception as e: - logger.error(f"Error DELETE /documents: {str(e)}") + error_msg = f"Error clearing documents: {str(e)}" + logger.error(error_msg) logger.error(traceback.format_exc()) + if "history_messages" in pipeline_status: + pipeline_status["history_messages"].append(error_msg) raise HTTPException(status_code=500, detail=str(e)) + finally: + # Reset busy status after completion + async with pipeline_status_lock: + pipeline_status["busy"] = False + completion_msg = "Document clearing process completed" + pipeline_status["latest_message"] = completion_msg + if "history_messages" in pipeline_status: + pipeline_status["history_messages"].append(completion_msg) @router.get( "/pipeline_status", @@ -850,7 +1208,9 @@ def create_document_routes( logger.error(traceback.format_exc()) raise HTTPException(status_code=500, detail=str(e)) - @router.get("", dependencies=[Depends(combined_auth)]) + @router.get( + "", response_model=DocsStatusesResponse, dependencies=[Depends(combined_auth)] + ) async def documents() -> DocsStatusesResponse: """ Get the status of all documents in the system. @@ -908,4 +1268,57 @@ def create_document_routes( logger.error(traceback.format_exc()) raise HTTPException(status_code=500, detail=str(e)) + @router.post( + "/clear_cache", + response_model=ClearCacheResponse, + dependencies=[Depends(combined_auth)], + ) + async def clear_cache(request: ClearCacheRequest): + """ + Clear cache data from the LLM response cache storage. + + This endpoint allows clearing specific modes of cache or all cache if no modes are specified. + Valid modes include: "default", "naive", "local", "global", "hybrid", "mix". + - "default" represents extraction cache. + - Other modes correspond to different query modes. + + Args: + request (ClearCacheRequest): The request body containing optional modes to clear. + + Returns: + ClearCacheResponse: A response object containing the status and message. + + Raises: + HTTPException: If an error occurs during cache clearing (400 for invalid modes, 500 for other errors). + """ + try: + # Validate modes if provided + valid_modes = ["default", "naive", "local", "global", "hybrid", "mix"] + if request.modes and not all(mode in valid_modes for mode in request.modes): + invalid_modes = [ + mode for mode in request.modes if mode not in valid_modes + ] + raise HTTPException( + status_code=400, + detail=f"Invalid mode(s): {invalid_modes}. Valid modes are: {valid_modes}", + ) + + # Call the aclear_cache method + await rag.aclear_cache(request.modes) + + # Prepare success message + if request.modes: + message = f"Successfully cleared cache for modes: {request.modes}" + else: + message = "Successfully cleared all cache" + + return ClearCacheResponse(status="success", message=message) + except HTTPException: + # Re-raise HTTP exceptions + raise + except Exception as e: + logger.error(f"Error clearing cache: {str(e)}") + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) + return router diff --git a/lightrag/api/routers/graph_routes.py b/lightrag/api/routers/graph_routes.py index f9d77ff6..381df90b 100644 --- a/lightrag/api/routers/graph_routes.py +++ b/lightrag/api/routers/graph_routes.py @@ -3,7 +3,7 @@ This module contains all graph-related routes for the LightRAG API. """ from typing import Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query from ..utils_api import get_combined_auth_dependency @@ -25,23 +25,20 @@ def create_graph_routes(rag, api_key: Optional[str] = None): @router.get("/graphs", dependencies=[Depends(combined_auth)]) async def get_knowledge_graph( - label: str, max_depth: int = 3, min_degree: int = 0, inclusive: bool = False + label: str = Query(..., description="Label to get knowledge graph for"), + max_depth: int = Query(3, description="Maximum depth of graph", ge=1), + max_nodes: int = Query(1000, description="Maximum nodes to return", ge=1), ): """ Retrieve a connected subgraph of nodes where the label includes the specified label. - Maximum number of nodes is constrained by the environment variable `MAX_GRAPH_NODES` (default: 1000). When reducing the number of nodes, the prioritization criteria are as follows: - 1. min_degree does not affect nodes directly connected to the matching nodes - 2. Label matching nodes take precedence - 3. Followed by nodes directly connected to the matching nodes - 4. Finally, the degree of the nodes - Maximum number of nodes is limited to env MAX_GRAPH_NODES(default: 1000) + 1. Hops(path) to the staring node take precedence + 2. Followed by the degree of the nodes Args: - label (str): Label to get knowledge graph for - max_depth (int, optional): Maximum depth of graph. Defaults to 3. - inclusive_search (bool, optional): If True, search for nodes that include the label. Defaults to False. - min_degree (int, optional): Minimum degree of nodes. Defaults to 0. + label (str): Label of the starting node + max_depth (int, optional): Maximum depth of the subgraph,Defaults to 3 + max_nodes: Maxiumu nodes to return Returns: Dict[str, List[str]]: Knowledge graph for label @@ -49,8 +46,7 @@ def create_graph_routes(rag, api_key: Optional[str] = None): return await rag.get_knowledge_graph( node_label=label, max_depth=max_depth, - inclusive=inclusive, - min_degree=min_degree, + max_nodes=max_nodes, ) return router diff --git a/lightrag/api/run_with_gunicorn.py b/lightrag/api/run_with_gunicorn.py index 065a12a1..cf902a8a 100644 --- a/lightrag/api/run_with_gunicorn.py +++ b/lightrag/api/run_with_gunicorn.py @@ -7,14 +7,9 @@ import os import sys import signal import pipmaster as pm -from lightrag.api.utils_api import parse_args, display_splash_screen, check_env_file +from lightrag.api.utils_api import display_splash_screen, check_env_file from lightrag.kg.shared_storage import initialize_share_data, finalize_share_data -from dotenv import load_dotenv - -# use the .env that is inside the current folder -# allows to use different .env file for each lightrag instance -# the OS environment variables take precedence over the .env file -load_dotenv(dotenv_path=".env", override=False) +from .config import global_args def check_and_install_dependencies(): @@ -59,20 +54,17 @@ def main(): signal.signal(signal.SIGINT, signal_handler) # Ctrl+C signal.signal(signal.SIGTERM, signal_handler) # kill command - # Parse all arguments using parse_args - args = parse_args(is_uvicorn_mode=False) - # Display startup information - display_splash_screen(args) + display_splash_screen(global_args) print("🚀 Starting LightRAG with Gunicorn") - print(f"🔄 Worker management: Gunicorn (workers={args.workers})") + print(f"🔄 Worker management: Gunicorn (workers={global_args.workers})") print("🔍 Preloading app: Enabled") print("📝 Note: Using Gunicorn's preload feature for shared data initialization") print("\n\n" + "=" * 80) print("MAIN PROCESS INITIALIZATION") print(f"Process ID: {os.getpid()}") - print(f"Workers setting: {args.workers}") + print(f"Workers setting: {global_args.workers}") print("=" * 80 + "\n") # Import Gunicorn's StandaloneApplication @@ -128,31 +120,43 @@ def main(): # Set configuration variables in gunicorn_config, prioritizing command line arguments gunicorn_config.workers = ( - args.workers if args.workers else int(os.getenv("WORKERS", 1)) + global_args.workers + if global_args.workers + else int(os.getenv("WORKERS", 1)) ) # Bind configuration prioritizes command line arguments - host = args.host if args.host != "0.0.0.0" else os.getenv("HOST", "0.0.0.0") - port = args.port if args.port != 9621 else int(os.getenv("PORT", 9621)) + host = ( + global_args.host + if global_args.host != "0.0.0.0" + else os.getenv("HOST", "0.0.0.0") + ) + port = ( + global_args.port + if global_args.port != 9621 + else int(os.getenv("PORT", 9621)) + ) gunicorn_config.bind = f"{host}:{port}" # Log level configuration prioritizes command line arguments gunicorn_config.loglevel = ( - args.log_level.lower() - if args.log_level + global_args.log_level.lower() + if global_args.log_level else os.getenv("LOG_LEVEL", "info") ) # Timeout configuration prioritizes command line arguments gunicorn_config.timeout = ( - args.timeout if args.timeout * 2 else int(os.getenv("TIMEOUT", 150 * 2)) + global_args.timeout + if global_args.timeout * 2 + else int(os.getenv("TIMEOUT", 150 * 2)) ) # Keepalive configuration gunicorn_config.keepalive = int(os.getenv("KEEPALIVE", 5)) # SSL configuration prioritizes command line arguments - if args.ssl or os.getenv("SSL", "").lower() in ( + if global_args.ssl or os.getenv("SSL", "").lower() in ( "true", "1", "yes", @@ -160,12 +164,14 @@ def main(): "on", ): gunicorn_config.certfile = ( - args.ssl_certfile - if args.ssl_certfile + global_args.ssl_certfile + if global_args.ssl_certfile else os.getenv("SSL_CERTFILE") ) gunicorn_config.keyfile = ( - args.ssl_keyfile if args.ssl_keyfile else os.getenv("SSL_KEYFILE") + global_args.ssl_keyfile + if global_args.ssl_keyfile + else os.getenv("SSL_KEYFILE") ) # Set configuration options from the module @@ -190,13 +196,13 @@ def main(): # Import the application from lightrag.api.lightrag_server import get_application - return get_application(args) + return get_application(global_args) # Create the application app = GunicornApp("") # Force workers to be an integer and greater than 1 for multi-process mode - workers_count = int(args.workers) + workers_count = int(global_args.workers) if workers_count > 1: # Set a flag to indicate we're in the main process os.environ["LIGHTRAG_MAIN_PROCESS"] = "1" diff --git a/lightrag/api/utils_api.py b/lightrag/api/utils_api.py index c01b7a37..ad75fd4e 100644 --- a/lightrag/api/utils_api.py +++ b/lightrag/api/utils_api.py @@ -7,15 +7,13 @@ import argparse from typing import Optional, List, Tuple import sys from ascii_colors import ASCIIColors -import logging from lightrag.api import __api_version__ as api_version from lightrag import __version__ as core_version from fastapi import HTTPException, Security, Request, status -from dotenv import load_dotenv from fastapi.security import APIKeyHeader, OAuth2PasswordBearer from starlette.status import HTTP_403_FORBIDDEN from .auth import auth_handler -from ..prompt import PROMPTS +from .config import ollama_server_infos, global_args def check_env_file(): @@ -36,16 +34,8 @@ def check_env_file(): return True -# use the .env that is inside the current folder -# allows to use different .env file for each lightrag instance -# the OS environment variables take precedence over the .env file -load_dotenv(dotenv_path=".env", override=False) - -global_args = {"main_args": None} - -# Get whitelist paths from environment variable, only once during initialization -default_whitelist = "/health,/api/*" -whitelist_paths = os.getenv("WHITELIST_PATHS", default_whitelist).split(",") +# Get whitelist paths from global_args, only once during initialization +whitelist_paths = global_args.whitelist_paths.split(",") # Pre-compile path matching patterns whitelist_patterns: List[Tuple[str, bool]] = [] @@ -63,19 +53,6 @@ for path in whitelist_paths: auth_configured = bool(auth_handler.accounts) -class OllamaServerInfos: - # Constants for emulated Ollama model information - LIGHTRAG_NAME = "lightrag" - LIGHTRAG_TAG = os.getenv("OLLAMA_EMULATING_MODEL_TAG", "latest") - LIGHTRAG_MODEL = f"{LIGHTRAG_NAME}:{LIGHTRAG_TAG}" - LIGHTRAG_SIZE = 7365960935 # it's a dummy value - LIGHTRAG_CREATED_AT = "2024-01-15T00:00:00Z" - LIGHTRAG_DIGEST = "sha256:lightrag" - - -ollama_server_infos = OllamaServerInfos() - - def get_combined_auth_dependency(api_key: Optional[str] = None): """ Create a combined authentication dependency that implements authentication logic @@ -186,299 +163,6 @@ def get_combined_auth_dependency(api_key: Optional[str] = None): return combined_dependency -class DefaultRAGStorageConfig: - KV_STORAGE = "JsonKVStorage" - VECTOR_STORAGE = "NanoVectorDBStorage" - GRAPH_STORAGE = "NetworkXStorage" - DOC_STATUS_STORAGE = "JsonDocStatusStorage" - - -def get_default_host(binding_type: str) -> str: - default_hosts = { - "ollama": os.getenv("LLM_BINDING_HOST", "http://localhost:11434"), - "lollms": os.getenv("LLM_BINDING_HOST", "http://localhost:9600"), - "azure_openai": os.getenv("AZURE_OPENAI_ENDPOINT", "https://api.openai.com/v1"), - "openai": os.getenv("LLM_BINDING_HOST", "https://api.openai.com/v1"), - } - return default_hosts.get( - binding_type, os.getenv("LLM_BINDING_HOST", "http://localhost:11434") - ) # fallback to ollama if unknown - - -def get_env_value(env_key: str, default: any, value_type: type = str) -> any: - """ - Get value from environment variable with type conversion - - Args: - env_key (str): Environment variable key - default (any): Default value if env variable is not set - value_type (type): Type to convert the value to - - Returns: - any: Converted value from environment or default - """ - value = os.getenv(env_key) - if value is None: - return default - - if value_type is bool: - return value.lower() in ("true", "1", "yes", "t", "on") - try: - return value_type(value) - except ValueError: - return default - - -def parse_args(is_uvicorn_mode: bool = False) -> argparse.Namespace: - """ - Parse command line arguments with environment variable fallback - - Args: - is_uvicorn_mode: Whether running under uvicorn mode - - Returns: - argparse.Namespace: Parsed arguments - """ - - parser = argparse.ArgumentParser( - description="LightRAG FastAPI Server with separate working and input directories" - ) - - # Server configuration - parser.add_argument( - "--host", - default=get_env_value("HOST", "0.0.0.0"), - help="Server host (default: from env or 0.0.0.0)", - ) - parser.add_argument( - "--port", - type=int, - default=get_env_value("PORT", 9621, int), - help="Server port (default: from env or 9621)", - ) - - # Directory configuration - parser.add_argument( - "--working-dir", - default=get_env_value("WORKING_DIR", "./rag_storage"), - help="Working directory for RAG storage (default: from env or ./rag_storage)", - ) - parser.add_argument( - "--input-dir", - default=get_env_value("INPUT_DIR", "./inputs"), - help="Directory containing input documents (default: from env or ./inputs)", - ) - - def timeout_type(value): - if value is None: - return 150 - if value is None or value == "None": - return None - return int(value) - - parser.add_argument( - "--timeout", - default=get_env_value("TIMEOUT", None, timeout_type), - type=timeout_type, - help="Timeout in seconds (useful when using slow AI). Use None for infinite timeout", - ) - - # RAG configuration - parser.add_argument( - "--max-async", - type=int, - default=get_env_value("MAX_ASYNC", 4, int), - help="Maximum async operations (default: from env or 4)", - ) - parser.add_argument( - "--max-tokens", - type=int, - default=get_env_value("MAX_TOKENS", 32768, int), - help="Maximum token size (default: from env or 32768)", - ) - - # Logging configuration - parser.add_argument( - "--log-level", - default=get_env_value("LOG_LEVEL", "INFO"), - choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], - help="Logging level (default: from env or INFO)", - ) - parser.add_argument( - "--verbose", - action="store_true", - default=get_env_value("VERBOSE", False, bool), - help="Enable verbose debug output(only valid for DEBUG log-level)", - ) - - parser.add_argument( - "--key", - type=str, - default=get_env_value("LIGHTRAG_API_KEY", None), - help="API key for authentication. This protects lightrag server against unauthorized access", - ) - - # Optional https parameters - parser.add_argument( - "--ssl", - action="store_true", - default=get_env_value("SSL", False, bool), - help="Enable HTTPS (default: from env or False)", - ) - parser.add_argument( - "--ssl-certfile", - default=get_env_value("SSL_CERTFILE", None), - help="Path to SSL certificate file (required if --ssl is enabled)", - ) - parser.add_argument( - "--ssl-keyfile", - default=get_env_value("SSL_KEYFILE", None), - help="Path to SSL private key file (required if --ssl is enabled)", - ) - - parser.add_argument( - "--history-turns", - type=int, - default=get_env_value("HISTORY_TURNS", 3, int), - help="Number of conversation history turns to include (default: from env or 3)", - ) - - # Search parameters - parser.add_argument( - "--top-k", - type=int, - default=get_env_value("TOP_K", 60, int), - help="Number of most similar results to return (default: from env or 60)", - ) - parser.add_argument( - "--cosine-threshold", - type=float, - default=get_env_value("COSINE_THRESHOLD", 0.2, float), - help="Cosine similarity threshold (default: from env or 0.4)", - ) - - # Ollama model name - parser.add_argument( - "--simulated-model-name", - type=str, - default=get_env_value( - "SIMULATED_MODEL_NAME", ollama_server_infos.LIGHTRAG_MODEL - ), - help="Number of conversation history turns to include (default: from env or 3)", - ) - - # Namespace - parser.add_argument( - "--namespace-prefix", - type=str, - default=get_env_value("NAMESPACE_PREFIX", ""), - help="Prefix of the namespace", - ) - - parser.add_argument( - "--auto-scan-at-startup", - action="store_true", - default=False, - help="Enable automatic scanning when the program starts", - ) - - # Server workers configuration - parser.add_argument( - "--workers", - type=int, - default=get_env_value("WORKERS", 1, int), - help="Number of worker processes (default: from env or 1)", - ) - - # LLM and embedding bindings - parser.add_argument( - "--llm-binding", - type=str, - default=get_env_value("LLM_BINDING", "ollama"), - choices=["lollms", "ollama", "openai", "openai-ollama", "azure_openai"], - help="LLM binding type (default: from env or ollama)", - ) - parser.add_argument( - "--embedding-binding", - type=str, - default=get_env_value("EMBEDDING_BINDING", "ollama"), - choices=["lollms", "ollama", "openai", "azure_openai"], - help="Embedding binding type (default: from env or ollama)", - ) - - args = parser.parse_args() - - # If in uvicorn mode and workers > 1, force it to 1 and log warning - if is_uvicorn_mode and args.workers > 1: - original_workers = args.workers - args.workers = 1 - # Log warning directly here - logging.warning( - f"In uvicorn mode, workers parameter was set to {original_workers}. Forcing workers=1" - ) - - # convert relative path to absolute path - args.working_dir = os.path.abspath(args.working_dir) - args.input_dir = os.path.abspath(args.input_dir) - - # Inject storage configuration from environment variables - args.kv_storage = get_env_value( - "LIGHTRAG_KV_STORAGE", DefaultRAGStorageConfig.KV_STORAGE - ) - args.doc_status_storage = get_env_value( - "LIGHTRAG_DOC_STATUS_STORAGE", DefaultRAGStorageConfig.DOC_STATUS_STORAGE - ) - args.graph_storage = get_env_value( - "LIGHTRAG_GRAPH_STORAGE", DefaultRAGStorageConfig.GRAPH_STORAGE - ) - args.vector_storage = get_env_value( - "LIGHTRAG_VECTOR_STORAGE", DefaultRAGStorageConfig.VECTOR_STORAGE - ) - - # Get MAX_PARALLEL_INSERT from environment - args.max_parallel_insert = get_env_value("MAX_PARALLEL_INSERT", 2, int) - - # Handle openai-ollama special case - if args.llm_binding == "openai-ollama": - args.llm_binding = "openai" - args.embedding_binding = "ollama" - - args.llm_binding_host = get_env_value( - "LLM_BINDING_HOST", get_default_host(args.llm_binding) - ) - args.embedding_binding_host = get_env_value( - "EMBEDDING_BINDING_HOST", get_default_host(args.embedding_binding) - ) - args.llm_binding_api_key = get_env_value("LLM_BINDING_API_KEY", None) - args.embedding_binding_api_key = get_env_value("EMBEDDING_BINDING_API_KEY", "") - - # Inject model configuration - args.llm_model = get_env_value("LLM_MODEL", "mistral-nemo:latest") - args.embedding_model = get_env_value("EMBEDDING_MODEL", "bge-m3:latest") - args.embedding_dim = get_env_value("EMBEDDING_DIM", 1024, int) - args.max_embed_tokens = get_env_value("MAX_EMBED_TOKENS", 8192, int) - - # Inject chunk configuration - args.chunk_size = get_env_value("CHUNK_SIZE", 1200, int) - args.chunk_overlap_size = get_env_value("CHUNK_OVERLAP_SIZE", 100, int) - - # Inject LLM cache configuration - args.enable_llm_cache_for_extract = get_env_value( - "ENABLE_LLM_CACHE_FOR_EXTRACT", True, bool - ) - - # Inject LLM temperature configuration - args.temperature = get_env_value("TEMPERATURE", 0.5, float) - - # Select Document loading tool (DOCLING, DEFAULT) - args.document_loading_engine = get_env_value("DOCUMENT_LOADING_ENGINE", "DEFAULT") - - ollama_server_infos.LIGHTRAG_MODEL = args.simulated_model_name - - global_args["main_args"] = args - return args - - def display_splash_screen(args: argparse.Namespace) -> None: """ Display a colorful splash screen showing LightRAG server configuration @@ -489,7 +173,7 @@ def display_splash_screen(args: argparse.Namespace) -> None: # Banner ASCIIColors.cyan(f""" ╔══════════════════════════════════════════════════════════════╗ - ║ 🚀 LightRAG Server v{core_version}/{api_version} ║ + ║ 🚀 LightRAG Server v{core_version}/{api_version} ║ ║ Fast, Lightweight RAG Server Implementation ║ ╚══════════════════════════════════════════════════════════════╝ """) @@ -503,7 +187,7 @@ def display_splash_screen(args: argparse.Namespace) -> None: ASCIIColors.white(" ├─ Workers: ", end="") ASCIIColors.yellow(f"{args.workers}") ASCIIColors.white(" ├─ CORS Origins: ", end="") - ASCIIColors.yellow(f"{os.getenv('CORS_ORIGINS', '*')}") + ASCIIColors.yellow(f"{args.cors_origins}") ASCIIColors.white(" ├─ SSL Enabled: ", end="") ASCIIColors.yellow(f"{args.ssl}") if args.ssl: @@ -519,8 +203,10 @@ def display_splash_screen(args: argparse.Namespace) -> None: ASCIIColors.yellow(f"{args.verbose}") ASCIIColors.white(" ├─ History Turns: ", end="") ASCIIColors.yellow(f"{args.history_turns}") - ASCIIColors.white(" └─ API Key: ", end="") + ASCIIColors.white(" ├─ API Key: ", end="") ASCIIColors.yellow("Set" if args.key else "Not Set") + ASCIIColors.white(" └─ JWT Auth: ", end="") + ASCIIColors.yellow("Enabled" if args.auth_accounts else "Disabled") # Directory Configuration ASCIIColors.magenta("\n📂 Directory Configuration:") @@ -558,10 +244,9 @@ def display_splash_screen(args: argparse.Namespace) -> None: ASCIIColors.yellow(f"{args.embedding_dim}") # RAG Configuration - summary_language = os.getenv("SUMMARY_LANGUAGE", PROMPTS["DEFAULT_LANGUAGE"]) ASCIIColors.magenta("\n⚙️ RAG Configuration:") ASCIIColors.white(" ├─ Summary Language: ", end="") - ASCIIColors.yellow(f"{summary_language}") + ASCIIColors.yellow(f"{args.summary_language}") ASCIIColors.white(" ├─ Max Parallel Insert: ", end="") ASCIIColors.yellow(f"{args.max_parallel_insert}") ASCIIColors.white(" ├─ Max Embed Tokens: ", end="") @@ -595,19 +280,17 @@ def display_splash_screen(args: argparse.Namespace) -> None: protocol = "https" if args.ssl else "http" if args.host == "0.0.0.0": ASCIIColors.magenta("\n🌐 Server Access Information:") - ASCIIColors.white(" ├─ Local Access: ", end="") + ASCIIColors.white(" ├─ WebUI (local): ", end="") ASCIIColors.yellow(f"{protocol}://localhost:{args.port}") ASCIIColors.white(" ├─ Remote Access: ", end="") ASCIIColors.yellow(f"{protocol}://:{args.port}") ASCIIColors.white(" ├─ API Documentation (local): ", end="") ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/docs") - ASCIIColors.white(" ├─ Alternative Documentation (local): ", end="") + ASCIIColors.white(" └─ Alternative Documentation (local): ", end="") ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/redoc") - ASCIIColors.white(" └─ WebUI (local): ", end="") - ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/webui") - ASCIIColors.yellow("\n📝 Note:") - ASCIIColors.white(""" Since the server is running on 0.0.0.0: + ASCIIColors.magenta("\n📝 Note:") + ASCIIColors.cyan(""" Since the server is running on 0.0.0.0: - Use 'localhost' or '127.0.0.1' for local access - Use your machine's IP address for remote access - To find your IP address: @@ -617,42 +300,24 @@ def display_splash_screen(args: argparse.Namespace) -> None: else: base_url = f"{protocol}://{args.host}:{args.port}" ASCIIColors.magenta("\n🌐 Server Access Information:") - ASCIIColors.white(" ├─ Base URL: ", end="") + ASCIIColors.white(" ├─ WebUI (local): ", end="") ASCIIColors.yellow(f"{base_url}") ASCIIColors.white(" ├─ API Documentation: ", end="") ASCIIColors.yellow(f"{base_url}/docs") ASCIIColors.white(" └─ Alternative Documentation: ", end="") ASCIIColors.yellow(f"{base_url}/redoc") - # Usage Examples - ASCIIColors.magenta("\n📚 Quick Start Guide:") - ASCIIColors.cyan(""" - 1. Access the Swagger UI: - Open your browser and navigate to the API documentation URL above - - 2. API Authentication:""") - if args.key: - ASCIIColors.cyan(""" Add the following header to your requests: - X-API-Key: - """) - else: - ASCIIColors.cyan(" No authentication required\n") - - ASCIIColors.cyan(""" 3. Basic Operations: - - POST /upload_document: Upload new documents to RAG - - POST /query: Query your document collection - - 4. Monitor the server: - - Check server logs for detailed operation information - - Use healthcheck endpoint: GET /health - """) - # Security Notice if args.key: ASCIIColors.yellow("\n⚠️ Security Notice:") ASCIIColors.white(""" API Key authentication is enabled. Make sure to include the X-API-Key header in all your requests. """) + if args.auth_accounts: + ASCIIColors.yellow("\n⚠️ Security Notice:") + ASCIIColors.white(""" JWT authentication is enabled. + Make sure to login before making the request, and include the 'Authorization' in the header. + """) # Ensure splash output flush to system log sys.stdout.flush() diff --git a/lightrag/api/webui/assets/index-CD5HxTy1.css b/lightrag/api/webui/assets/index-CD5HxTy1.css deleted file mode 100644 index a0ab321b..00000000 --- a/lightrag/api/webui/assets/index-CD5HxTy1.css +++ /dev/null @@ -1 +0,0 @@ -/*! tailwindcss v4.0.8 | MIT License | https://tailwindcss.com */@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-100:oklch(.936 .032 17.717);--color-red-400:oklch(.704 .191 22.216);--color-red-500:oklch(.637 .237 25.331);--color-red-600:oklch(.577 .245 27.325);--color-red-950:oklch(.258 .092 26.042);--color-amber-100:oklch(.962 .059 95.617);--color-amber-200:oklch(.924 .12 95.746);--color-amber-700:oklch(.555 .163 48.998);--color-amber-800:oklch(.473 .137 46.201);--color-amber-900:oklch(.414 .112 45.904);--color-yellow-600:oklch(.681 .162 75.834);--color-green-500:oklch(.723 .219 149.579);--color-green-600:oklch(.627 .194 149.214);--color-emerald-50:oklch(.979 .021 166.113);--color-emerald-400:oklch(.765 .177 163.223);--color-emerald-700:oklch(.508 .118 165.612);--color-teal-100:oklch(.953 .051 180.801);--color-blue-600:oklch(.546 .245 262.881);--color-blue-700:oklch(.488 .243 264.376);--color-violet-700:oklch(.491 .27 292.581);--color-gray-100:oklch(.967 .003 264.542);--color-gray-200:oklch(.928 .006 264.531);--color-gray-300:oklch(.872 .01 258.338);--color-gray-400:oklch(.707 .022 261.325);--color-gray-500:oklch(.551 .027 264.364);--color-gray-600:oklch(.446 .03 256.802);--color-gray-700:oklch(.373 .034 259.733);--color-gray-800:oklch(.278 .033 256.848);--color-gray-900:oklch(.21 .034 264.665);--color-zinc-50:oklch(.985 0 0);--color-zinc-100:oklch(.967 .001 286.375);--color-zinc-200:oklch(.92 .004 286.32);--color-zinc-300:oklch(.871 .006 286.286);--color-zinc-600:oklch(.442 .017 285.786);--color-zinc-700:oklch(.37 .013 285.805);--color-zinc-800:oklch(.274 .006 286.033);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-lg:32rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-widest:.1em;--leading-relaxed:1.625;--radius-xs:.125rem;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--blur-sm:8px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-font-feature-settings:var(--font-sans--font-feature-settings);--default-font-variation-settings:var(--font-sans--font-variation-settings);--default-mono-font-family:var(--font-mono);--default-mono-font-feature-settings:var(--font-mono--font-feature-settings);--default-mono-font-variation-settings:var(--font-mono--font-variation-settings)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}body{line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1;color:color-mix(in oklab,currentColor 50%,transparent)}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:color-mix(in oklab,var(--ring)50%,transparent)}body{background-color:var(--background);color:var(--foreground)}*{scrollbar-color:initial;scrollbar-width:initial}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.inset-\[-1px\]{top:-1px;right:-1px;bottom:-1px;left:-1px}.top-0{top:calc(var(--spacing)*0)}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing)*2)}.top-4{top:calc(var(--spacing)*4)}.top-\[50\%\]{top:50%}.right-0{right:calc(var(--spacing)*0)}.right-2{right:calc(var(--spacing)*2)}.right-4{right:calc(var(--spacing)*4)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-2{bottom:calc(var(--spacing)*2)}.bottom-4{bottom:calc(var(--spacing)*4)}.\!left-1\/2{left:50%!important}.\!left-\[25\%\]{left:25%!important}.\!left-\[75\%\]{left:75%!important}.left-0{left:calc(var(--spacing)*0)}.left-2{left:calc(var(--spacing)*2)}.left-\[50\%\]{left:50%}.left-\[calc\(1rem\+2\.5rem\)\]{left:3.5rem}.z-10{z-index:10}.z-50{z-index:50}.z-60{z-index:60}.\!container{width:100%!important}@media (width>=40rem){.\!container{max-width:40rem!important}}@media (width>=48rem){.\!container{max-width:48rem!important}}@media (width>=64rem){.\!container{max-width:64rem!important}}@media (width>=80rem){.\!container{max-width:80rem!important}}@media (width>=96rem){.\!container{max-width:96rem!important}}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.\!m-0{margin:calc(var(--spacing)*0)!important}.m-0{margin:calc(var(--spacing)*0)}.\!mx-4{margin-inline:calc(var(--spacing)*4)!important}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.mx-1{margin-inline:calc(var(--spacing)*1)}.mx-4{margin-inline:calc(var(--spacing)*4)}.my-1{margin-block:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-2{margin-right:calc(var(--spacing)*2)}.mr-4{margin-right:calc(var(--spacing)*4)}.mr-8{margin-right:calc(var(--spacing)*8)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\!inline{display:inline!important}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.\!size-full{width:100%!important;height:100%!important}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.size-7{width:calc(var(--spacing)*7);height:calc(var(--spacing)*7)}.size-8{width:calc(var(--spacing)*8);height:calc(var(--spacing)*8)}.size-10{width:calc(var(--spacing)*10);height:calc(var(--spacing)*10)}.size-full{width:100%;height:100%}.h-1\/2{height:50%}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-12{height:calc(var(--spacing)*12)}.h-24{height:calc(var(--spacing)*24)}.h-52{height:calc(var(--spacing)*52)}.h-\[1px\]{height:1px}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-fit{height:fit-content}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-8{max-height:calc(var(--spacing)*8)}.max-h-48{max-height:calc(var(--spacing)*48)}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[40vh\]{max-height:40vh}.max-h-\[60vh\]{max-height:60vh}.max-h-\[300px\]{max-height:300px}.max-h-full{max-height:100%}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-\[7\.5em\]{min-height:7.5em}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-12{width:calc(var(--spacing)*12)}.w-16{width:calc(var(--spacing)*16)}.w-24{width:calc(var(--spacing)*24)}.w-56{width:calc(var(--spacing)*56)}.w-\[1px\]{width:1px}.w-\[200px\]{width:200px}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.max-w-80{max-width:calc(var(--spacing)*80)}.max-w-\[80\%\]{max-width:80%}.max-w-\[250px\]{max-width:250px}.max-w-\[480px\]{max-width:480px}.max-w-lg{max-width:var(--container-lg)}.max-w-none{max-width:none}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-45{min-width:calc(var(--spacing)*45)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[180px\]{min-width:180px}.min-w-\[300px\]{min-width:300px}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.flex-none{flex:none}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.caption-bottom{caption-side:bottom}.\!-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)!important}.\!translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)!important}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-125{--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x)var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x)var(--tw-rotate-y)var(--tw-rotate-z)var(--tw-skew-x)var(--tw-skew-y)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.\[appearance\:textfield\]{-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.place-items-center{place-items:center}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-px{gap:1px}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.self-center{align-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.\!overflow-hidden{overflow:hidden!important}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.\!rounded-none{border-radius:0!important}.rounded{border-radius:.25rem}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.rounded-xs{border-radius:var(--radius-xs)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-tr-none{border-top-right-radius:0}.rounded-br-none{border-bottom-right-radius:0}.border,.border-1{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-input{border-color:var(--input)!important}.border-border\/40{border-color:color-mix(in oklab,var(--border)40%,transparent)}.border-destructive\/50{border-color:color-mix(in oklab,var(--destructive)50%,transparent)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-400{border-color:var(--color-gray-400)}.border-input{border-color:var(--input)}.border-muted-foreground\/25{border-color:color-mix(in oklab,var(--muted-foreground)25%,transparent)}.border-muted-foreground\/50{border-color:color-mix(in oklab,var(--muted-foreground)50%,transparent)}.border-primary{border-color:var(--primary)}.border-transparent{border-color:#0000}.border-t-transparent{border-top-color:#0000}.border-l-transparent{border-left-color:#0000}.\!bg-background{background-color:var(--background)!important}.\!bg-emerald-400{background-color:var(--color-emerald-400)!important}.bg-amber-100{background-color:var(--color-amber-100)}.bg-background{background-color:var(--background)}.bg-background\/60{background-color:color-mix(in oklab,var(--background)60%,transparent)}.bg-background\/80{background-color:color-mix(in oklab,var(--background)80%,transparent)}.bg-background\/95{background-color:color-mix(in oklab,var(--background)95%,transparent)}.bg-black\/10{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}.bg-black\/30{background-color:color-mix(in oklab,var(--color-black)30%,transparent)}.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}.bg-border{background-color:var(--border)}.bg-card{background-color:var(--card)}.bg-card\/95{background-color:color-mix(in oklab,var(--card)95%,transparent)}.bg-destructive{background-color:var(--destructive)}.bg-foreground\/10{background-color:color-mix(in oklab,var(--foreground)10%,transparent)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-green-500{background-color:var(--color-green-500)}.bg-muted{background-color:var(--muted)}.bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}.bg-popover{background-color:var(--popover)}.bg-primary{background-color:var(--primary)}.bg-primary-foreground\/60{background-color:color-mix(in oklab,var(--primary-foreground)60%,transparent)}.bg-primary\/5{background-color:color-mix(in oklab,var(--primary)5%,transparent)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-400{background-color:var(--color-red-400)}.bg-red-500{background-color:var(--color-red-500)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-white\/30{background-color:color-mix(in oklab,var(--color-white)30%,transparent)}.bg-zinc-200{background-color:var(--color-zinc-200)}.bg-zinc-800{background-color:var(--color-zinc-800)}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-emerald-50{--tw-gradient-from:var(--color-emerald-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-teal-100{--tw-gradient-to:var(--color-teal-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.object-cover{object-fit:cover}.\!p-0{padding:calc(var(--spacing)*0)!important}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-16{padding:calc(var(--spacing)*16)}.p-\[1px\]{padding:1px}.px-1{padding-inline:calc(var(--spacing)*1)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-6{padding-block:calc(var(--spacing)*6)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-6{padding-top:calc(var(--spacing)*6)}.pr-1{padding-right:calc(var(--spacing)*1)}.pr-2{padding-right:calc(var(--spacing)*2)}.pb-1{padding-bottom:calc(var(--spacing)*1)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-8{padding-bottom:calc(var(--spacing)*8)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pl-1{padding-left:calc(var(--spacing)*1)}.pl-8{padding-left:calc(var(--spacing)*8)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.\!text-zinc-50{color:var(--color-zinc-50)!important}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-card-foreground{color:var(--card-foreground)}.text-current{color:currentColor}.text-destructive{color:var(--destructive)}.text-destructive-foreground{color:var(--destructive-foreground)}.text-emerald-400{color:var(--color-emerald-400)}.text-emerald-700{color:var(--color-emerald-700)}.text-foreground{color:var(--foreground)}.text-foreground\/80{color:color-mix(in oklab,var(--foreground)80%,transparent)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-700{color:var(--color-gray-700)}.text-gray-900{color:var(--color-gray-900)}.text-green-600{color:var(--color-green-600)}.text-muted-foreground{color:var(--muted-foreground)}.text-muted-foreground\/70{color:color-mix(in oklab,var(--muted-foreground)70%,transparent)}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-primary\/60{color:color-mix(in oklab,var(--primary)60%,transparent)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-violet-700{color:var(--color-violet-700)}.text-yellow-600{color:var(--color-yellow-600)}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-800{color:var(--color-zinc-800)}.underline-offset-4{text-underline-offset:4px}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_8px_rgba\(0\,0\,0\,0\.2\)\]{--tw-shadow:0 0 8px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(34\,197\,94\,0\.4\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#22c55e66);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(239\,68\,68\,0\.4\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#ef444466);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[inset_0_-1px_0_rgba\(0\,0\,0\,0\.1\)\]{--tw-shadow:inset 0 -1px 0 var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-2000{--tw-duration:2s;transition-duration:2s}.animate-in{--tw-enter-opacity:initial;--tw-enter-scale:initial;--tw-enter-rotate:initial;--tw-enter-translate-x:initial;--tw-enter-translate-y:initial;animation-name:enter;animation-duration:.15s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.duration-200{animation-duration:.2s}.duration-300{animation-duration:.3s}.duration-2000{animation-duration:2s}.fade-in-0{--tw-enter-opacity:0}.running{animation-play-state:running}.zoom-in-95{--tw-enter-scale:.95}@media (hover:hover){.group-hover\:visible:is(:where(.group):hover *){visibility:visible}}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-70:is(:where(.peer):disabled~*){opacity:.7}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}@media (hover:hover){.hover\:w-fit:hover{width:fit-content}.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-background\/60:hover{background-color:color-mix(in oklab,var(--background)60%,transparent)}.hover\:bg-destructive\/80:hover{background-color:color-mix(in oklab,var(--destructive)80%,transparent)}.hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-muted\/25:hover{background-color:color-mix(in oklab,var(--muted)25%,transparent)}.hover\:bg-muted\/50:hover{background-color:color-mix(in oklab,var(--muted)50%,transparent)}.hover\:bg-primary\/5:hover{background-color:color-mix(in oklab,var(--primary)5%,transparent)}.hover\:bg-primary\/20:hover{background-color:color-mix(in oklab,var(--primary)20%,transparent)}.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab,var(--primary)80%,transparent)}.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}.hover\:bg-zinc-300:hover{background-color:var(--color-zinc-300)}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:text-accent-foreground:focus{color:var(--accent-foreground)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:outline-0:focus{outline-style:var(--tw-outline-style);outline-width:0}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:relative:focus-visible{position:relative}.focus-visible\:ring-1:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentColor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--ring)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:right-0:active{right:calc(var(--spacing)*0)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[disabled\=true\]\:pointer-events-none[data-disabled=true]{pointer-events:none}.data-\[disabled\=true\]\:opacity-50[data-disabled=true]{opacity:.5}.data-\[selected\=\'true\'\]\:bg-accent[data-selected=true]{background-color:var(--accent)}.data-\[selected\=true\]\:text-accent-foreground[data-selected=true]{color:var(--accent-foreground)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:-.5rem}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:.5rem}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:-.5rem}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:.5rem}.data-\[state\=active\]\:visible[data-state=active]{visibility:visible}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--background)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--foreground)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:var(--primary)}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:var(--primary-foreground)}.data-\[state\=closed\]\:animate-out[data-state=closed]{--tw-exit-opacity:initial;--tw-exit-scale:initial;--tw-exit-rotate:initial;--tw-exit-translate-x:initial;--tw-exit-translate-y:initial;animation-name:exit;animation-duration:.15s}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y:-48%}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale:.95}.data-\[state\=inactive\]\:invisible[data-state=inactive]{visibility:hidden}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:var(--accent)}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:var(--muted-foreground)}.data-\[state\=open\]\:animate-in[data-state=open]{--tw-enter-opacity:initial;--tw-enter-scale:initial;--tw-enter-rotate:initial;--tw-enter-translate-x:initial;--tw-enter-translate-y:initial;animation-name:enter;animation-duration:.15s}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity:0}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y:-48%}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale:.95}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:var(--muted)}@supports ((-webkit-backdrop-filter:var(--tw)) or (backdrop-filter:var(--tw))){.supports-\[backdrop-filter\]\:bg-background\/60{background-color:color-mix(in oklab,var(--background)60%,transparent)}.supports-\[backdrop-filter\]\:bg-card\/75{background-color:color-mix(in oklab,var(--card)75%,transparent)}}@media (width>=40rem){.sm\:mt-0{margin-top:calc(var(--spacing)*0)}.sm\:max-w-\[600px\]{max-width:600px}.sm\:max-w-xl{max-width:var(--container-xl)}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:px-5{padding-inline:calc(var(--spacing)*5)}.sm\:text-left{text-align:left}}@media (width>=48rem){.md\:inline-block{display:inline-block}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.dark\:border-destructive:is(.dark *){border-color:var(--destructive)}.dark\:border-gray-600:is(.dark *){border-color:var(--color-gray-600)}.dark\:border-gray-700:is(.dark *){border-color:var(--color-gray-700)}.dark\:bg-amber-900:is(.dark *){background-color:var(--color-amber-900)}.dark\:bg-gray-100\/20:is(.dark *){background-color:color-mix(in oklab,var(--color-gray-100)20%,transparent)}.dark\:bg-gray-800\/30:is(.dark *){background-color:color-mix(in oklab,var(--color-gray-800)30%,transparent)}.dark\:bg-red-950:is(.dark *){background-color:var(--color-red-950)}.dark\:bg-zinc-700:is(.dark *){background-color:var(--color-zinc-700)}.dark\:from-gray-900:is(.dark *){--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:to-gray-800:is(.dark *){--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark\:text-amber-200:is(.dark *){color:var(--color-amber-200)}.dark\:text-gray-300:is(.dark *){color:var(--color-gray-300)}.dark\:text-gray-400:is(.dark *){color:var(--color-gray-400)}.dark\:text-red-400:is(.dark *){color:var(--color-red-400)}.dark\:text-zinc-200:is(.dark *){color:var(--color-zinc-200)}@media (hover:hover){.dark\:hover\:bg-gray-700:is(.dark *):hover{background-color:var(--color-gray-700)}.dark\:hover\:bg-gray-800:is(.dark *):hover{background-color:var(--color-gray-800)}.dark\:hover\:bg-zinc-600:is(.dark *):hover{background-color:var(--color-zinc-600)}}.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading]{padding-inline:calc(var(--spacing)*2)}.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading]{padding-block:calc(var(--spacing)*1.5)}.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading]{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading]{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.\[\&_\[cmdk-group-heading\]\]\:text-muted-foreground [cmdk-group-heading]{color:var(--muted-foreground)}.\[\&_\[cmdk-group\]\]\:px-2 [cmdk-group]{padding-inline:calc(var(--spacing)*2)}.\[\&_\[cmdk-group\]\:not\(\[hidden\]\)_\~\[cmdk-group\]\]\:pt-0 [cmdk-group]:not([hidden])~[cmdk-group]{padding-top:calc(var(--spacing)*0)}.\[\&_\[cmdk-input-wrapper\]_svg\]\:h-5 [cmdk-input-wrapper] svg{height:calc(var(--spacing)*5)}.\[\&_\[cmdk-input-wrapper\]_svg\]\:w-5 [cmdk-input-wrapper] svg{width:calc(var(--spacing)*5)}.\[\&_\[cmdk-input\]\]\:h-12 [cmdk-input]{height:calc(var(--spacing)*12)}.\[\&_\[cmdk-item\]\]\:px-2 [cmdk-item]{padding-inline:calc(var(--spacing)*2)}.\[\&_\[cmdk-item\]\]\:py-3 [cmdk-item]{padding-block:calc(var(--spacing)*3)}.\[\&_\[cmdk-item\]_svg\]\:h-5 [cmdk-item] svg{height:calc(var(--spacing)*5)}.\[\&_\[cmdk-item\]_svg\]\:w-5 [cmdk-item] svg{width:calc(var(--spacing)*5)}.\[\&_p\]\:leading-relaxed p{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{-webkit-appearance:none;-moz-appearance:none;appearance:none}.\[\&\:\:-webkit-inner-spin-button\]\:opacity-100::-webkit-inner-spin-button{opacity:1}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{-webkit-appearance:none;-moz-appearance:none;appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:opacity-100::-webkit-outer-spin-button{opacity:1}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing)*0)}.\[\&\>\[role\=checkbox\]\]\:translate-y-\[2px\]>[role=checkbox]{--tw-translate-y:2px;translate:var(--tw-translate-x)var(--tw-translate-y)}.\[\&\>span\]\:line-clamp-1>span{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\[\&\>svg\]\:absolute>svg{position:absolute}.\[\&\>svg\]\:top-4>svg{top:calc(var(--spacing)*4)}.\[\&\>svg\]\:left-4>svg{left:calc(var(--spacing)*4)}.\[\&\>svg\]\:text-destructive>svg{color:var(--destructive)}.\[\&\>svg\]\:text-foreground>svg{color:var(--foreground)}.\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div{--tw-translate-y:-3px;translate:var(--tw-translate-x)var(--tw-translate-y)}.\[\&\>svg\~\*\]\:pl-7>svg~*{padding-left:calc(var(--spacing)*7)}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}}:root{--background:#fff;--foreground:#09090b;--card:#fff;--card-foreground:#09090b;--popover:#fff;--popover-foreground:#09090b;--primary:#18181b;--primary-foreground:#fafafa;--secondary:#f4f4f5;--secondary-foreground:#18181b;--muted:#f4f4f5;--muted-foreground:#71717a;--accent:#f4f4f5;--accent-foreground:#18181b;--destructive:#ef4444;--destructive-foreground:#fafafa;--border:#e4e4e7;--input:#e4e4e7;--ring:#09090b;--chart-1:#e76e50;--chart-2:#2a9d90;--chart-3:#274754;--chart-4:#e8c468;--chart-5:#f4a462;--radius:.6rem;--sidebar-background:#fafafa;--sidebar-foreground:#3f3f46;--sidebar-primary:#18181b;--sidebar-primary-foreground:#fafafa;--sidebar-accent:#f4f4f5;--sidebar-accent-foreground:#18181b;--sidebar-border:#e5e7eb;--sidebar-ring:#3b82f6}.dark{--background:#09090b;--foreground:#fafafa;--card:#09090b;--card-foreground:#fafafa;--popover:#09090b;--popover-foreground:#fafafa;--primary:#fafafa;--primary-foreground:#18181b;--secondary:#27272a;--secondary-foreground:#fafafa;--muted:#27272a;--muted-foreground:#a1a1aa;--accent:#27272a;--accent-foreground:#fafafa;--destructive:#7f1d1d;--destructive-foreground:#fafafa;--border:#27272a;--input:#27272a;--ring:#d4d4d8;--chart-1:#2662d9;--chart-2:#2eb88a;--chart-3:#e88c30;--chart-4:#af57db;--chart-5:#e23670;--sidebar-background:#18181b;--sidebar-foreground:#f4f4f5;--sidebar-primary:#1d4ed8;--sidebar-primary-foreground:#fff;--sidebar-accent:#27272a;--sidebar-accent-foreground:#f4f4f5;--sidebar-border:#27272a;--sidebar-ring:#3b82f6}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:5px}::-webkit-scrollbar-track{background-color:#f2f2f2}.dark ::-webkit-scrollbar-thumb{background-color:#e6e6e6}.dark ::-webkit-scrollbar-track{background-color:#000}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0))}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false;initial-value:rotateX(0)}@property --tw-rotate-y{syntax:"*";inherits:false;initial-value:rotateY(0)}@property --tw-rotate-z{syntax:"*";inherits:false;initial-value:rotateZ(0)}@property --tw-skew-x{syntax:"*";inherits:false;initial-value:skewX(0)}@property --tw-skew-y{syntax:"*";inherits:false;initial-value:skewY(0)}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}:root{--sigma-background-color:#fff;--sigma-controls-background-color:#fff;--sigma-controls-background-color-hover:rgba(0,0,0,.2);--sigma-controls-border-color:rgba(0,0,0,.2);--sigma-controls-color:#000;--sigma-controls-zindex:100;--sigma-controls-margin:5px;--sigma-controls-size:30px}div.react-sigma{height:100%;width:100%;position:relative;background:var(--sigma-background-color)}div.sigma-container{height:100%;width:100%}.react-sigma-controls{position:absolute;z-index:var(--sigma-controls-zindex);border:2px solid var(--sigma-controls-border-color);border-radius:4px;color:var(--sigma-controls-color);background-color:var(--sigma-controls-background-color)}.react-sigma-controls.bottom-right{bottom:var(--sigma-controls-margin);right:var(--sigma-controls-margin)}.react-sigma-controls.bottom-left{bottom:var(--sigma-controls-margin);left:var(--sigma-controls-margin)}.react-sigma-controls.top-right{top:var(--sigma-controls-margin);right:var(--sigma-controls-margin)}.react-sigma-controls.top-left{top:var(--sigma-controls-margin);left:var(--sigma-controls-margin)}.react-sigma-controls:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.react-sigma-controls:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.react-sigma-control{width:var(--sigma-controls-size);height:var(--sigma-controls-size);line-height:var(--sigma-controls-size);background-color:var(--sigma-controls-background-color);border-bottom:1px solid var(--sigma-controls-border-color)}.react-sigma-control:last-child{border-bottom:none}.react-sigma-control>*{box-sizing:border-box}.react-sigma-control>button{display:block;border:none;margin:0;padding:0;width:var(--sigma-controls-size);height:var(--sigma-controls-size);line-height:var(--sigma-controls-size);background-position:center;background-size:50%;background-repeat:no-repeat;background-color:var(--sigma-controls-background-color);clip:rect(0,0,0,0)}.react-sigma-control>button:hover{background-color:var(--sigma-controls-background-color-hover)}.react-sigma-search{background-color:var(--sigma-controls-background-color)}.react-sigma-search label{visibility:hidden}.react-sigma-search input{color:var(--sigma-controls-color);background-color:var(--sigma-controls-background-color);font-size:1em;width:100%;margin:0;border:none;padding:var(--sigma-controls-margin);box-sizing:border-box}:root{--sigma-grey-color:#ccc}.react-sigma .option.hoverable{cursor:pointer!important}.react-sigma .text-ellipsis{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.react-sigma .react-select__clear-indicator{cursor:pointer!important}.react-sigma .text-muted{color:var(--sigma-grey-color)}.react-sigma .text-italic{font-style:italic}.react-sigma .text-center{text-align:center}.react-sigma .graph-search{min-width:250px}.react-sigma .graph-search .option{padding:2px 8px}.react-sigma .graph-search .dropdown-indicator{font-size:1.25em;padding:4px}.react-sigma .graph-search .option.selected{background-color:var(--sigma-grey-color)}.react-sigma .node .render{position:relative;display:inline-block;width:1em;height:1em;border-radius:1em;background-color:var(--sigma-grey-color);margin-right:8px}.react-sigma .node{display:flex;flex-direction:row;align-items:center}.react-sigma .node .render{flex-grow:0;flex-shrink:0;margin-right:0 .25em}.react-sigma .node .label{flex-grow:1;flex-shrink:1}.react-sigma .edge{display:flex;flex-direction:column;align-items:flex-start;flex-grow:0;flex-shrink:0;flex-wrap:nowrap}.react-sigma .edge .node{font-size:.7em}.react-sigma .edge .body{display:flex;flex-direction:row;flex-grow:1;flex-shrink:1;min-height:.6em}.react-sigma .edge .body .render{display:flex;flex-direction:column;margin:0 2px}.react-sigma .edge .body .render .dash,.react-sigma .edge .body .render .dotted{display:inline-block;width:0;margin:0 2px;border:2px solid #ccc;flex-grow:1;flex-shrink:1}.react-sigma .edge .body .render .dotted{border-style:dotted}.react-sigma .edge .body .render .arrow{width:0;height:0;border-left:.3em solid transparent;border-right:.3em solid transparent;border-top:.6em solid red;flex-shrink:0;flex-grow:0;border-left-width:.3em;border-right-width:.3em}.react-sigma .edge .body .label{flex-grow:1;flex-shrink:1;text-align:center} diff --git a/lightrag/api/webui/assets/index-Cma7xY0-.js b/lightrag/api/webui/assets/index-Cma7xY0-.js new file mode 100644 index 00000000..df32e654 --- /dev/null +++ b/lightrag/api/webui/assets/index-Cma7xY0-.js @@ -0,0 +1,1345 @@ +var aq=Object.defineProperty;var oq=(e,t,n)=>t in e?aq(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var Qr=(e,t,n)=>oq(e,typeof t!="symbol"?t+"":t,n);function iq(e,t){for(var n=0;nr[a]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))r(a);new MutationObserver(a=>{for(const o of a)if(o.type==="childList")for(const s of o.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(a){const o={};return a.integrity&&(o.integrity=a.integrity),a.referrerPolicy&&(o.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?o.credentials="include":a.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(a){if(a.ep)return;a.ep=!0;const o=n(a);fetch(a.href,o)}})();var ff=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function cn(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function sq(e){if(e.__esModule)return e;var t=e.default;if(typeof t=="function"){var n=function r(){return this instanceof r?Reflect.construct(t,arguments,this.constructor):t.apply(this,arguments)};n.prototype=t.prototype}else n={};return Object.defineProperty(n,"__esModule",{value:!0}),Object.keys(e).forEach(function(r){var a=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(n,r,a.get?a:{enumerable:!0,get:function(){return e[r]}})}),n}var Wh={exports:{}},Zl={};/** + * @license React + * react-jsx-runtime.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var JC;function lq(){if(JC)return Zl;JC=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function n(r,a,o){var s=null;if(o!==void 0&&(s=""+o),a.key!==void 0&&(s=""+a.key),"key"in a){o={};for(var u in a)u!=="key"&&(o[u]=a[u])}else o=a;return a=o.ref,{$$typeof:e,type:r,key:s,ref:a!==void 0?a:null,props:o}}return Zl.Fragment=t,Zl.jsx=n,Zl.jsxs=n,Zl}var e_;function uq(){return e_||(e_=1,Wh.exports=lq()),Wh.exports}var E=uq(),Yh={exports:{}},lt={};/** + * @license React + * react.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var t_;function cq(){if(t_)return lt;t_=1;var e=Symbol.for("react.transitional.element"),t=Symbol.for("react.portal"),n=Symbol.for("react.fragment"),r=Symbol.for("react.strict_mode"),a=Symbol.for("react.profiler"),o=Symbol.for("react.consumer"),s=Symbol.for("react.context"),u=Symbol.for("react.forward_ref"),c=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),g=Symbol.iterator;function m(M){return M===null||typeof M!="object"?null:(M=g&&M[g]||M["@@iterator"],typeof M=="function"?M:null)}var b={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},y=Object.assign,v={};function k(M,V,j){this.props=M,this.context=V,this.refs=v,this.updater=j||b}k.prototype.isReactComponent={},k.prototype.setState=function(M,V){if(typeof M!="object"&&typeof M!="function"&&M!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,M,V,"setState")},k.prototype.forceUpdate=function(M){this.updater.enqueueForceUpdate(this,M,"forceUpdate")};function A(){}A.prototype=k.prototype;function x(M,V,j){this.props=M,this.context=V,this.refs=v,this.updater=j||b}var R=x.prototype=new A;R.constructor=x,y(R,k.prototype),R.isPureReactComponent=!0;var O=Array.isArray,N={H:null,A:null,T:null,S:null},C=Object.prototype.hasOwnProperty;function _(M,V,j,P,K,ee){return j=ee.ref,{$$typeof:e,type:M,key:V,ref:j!==void 0?j:null,props:ee}}function L(M,V){return _(M.type,V,void 0,void 0,void 0,M.props)}function I(M){return typeof M=="object"&&M!==null&&M.$$typeof===e}function D(M){var V={"=":"=0",":":"=2"};return"$"+M.replace(/[=:]/g,function(j){return V[j]})}var G=/\/+/g;function $(M,V){return typeof M=="object"&&M!==null&&M.key!=null?D(""+M.key):V.toString(36)}function B(){}function W(M){switch(M.status){case"fulfilled":return M.value;case"rejected":throw M.reason;default:switch(typeof M.status=="string"?M.then(B,B):(M.status="pending",M.then(function(V){M.status==="pending"&&(M.status="fulfilled",M.value=V)},function(V){M.status==="pending"&&(M.status="rejected",M.reason=V)})),M.status){case"fulfilled":return M.value;case"rejected":throw M.reason}}throw M}function Q(M,V,j,P,K){var ee=typeof M;(ee==="undefined"||ee==="boolean")&&(M=null);var le=!1;if(M===null)le=!0;else switch(ee){case"bigint":case"string":case"number":le=!0;break;case"object":switch(M.$$typeof){case e:case t:le=!0;break;case p:return le=M._init,Q(le(M._payload),V,j,P,K)}}if(le)return K=K(M),le=P===""?"."+$(M,0):P,O(K)?(j="",le!=null&&(j=le.replace(G,"$&/")+"/"),Q(K,V,j,"",function(he){return he})):K!=null&&(I(K)&&(K=L(K,j+(K.key==null||M&&M.key===K.key?"":(""+K.key).replace(G,"$&/")+"/")+le)),V.push(K)),1;le=0;var X=P===""?".":P+":";if(O(M))for(var J=0;J>>1,M=H[Y];if(0>>1;Ya(P,F))Ka(ee,P)?(H[Y]=ee,H[K]=F,Y=K):(H[Y]=P,H[j]=F,Y=j);else if(Ka(ee,F))H[Y]=ee,H[K]=F,Y=K;else break e}}return U}function a(H,U){var F=H.sortIndex-U.sortIndex;return F!==0?F:H.id-U.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var s=Date,u=s.now();e.unstable_now=function(){return s.now()-u}}var c=[],d=[],p=1,g=null,m=3,b=!1,y=!1,v=!1,k=typeof setTimeout=="function"?setTimeout:null,A=typeof clearTimeout=="function"?clearTimeout:null,x=typeof setImmediate<"u"?setImmediate:null;function R(H){for(var U=n(d);U!==null;){if(U.callback===null)r(d);else if(U.startTime<=H)r(d),U.sortIndex=U.expirationTime,t(c,U);else break;U=n(d)}}function O(H){if(v=!1,R(H),!y)if(n(c)!==null)y=!0,W();else{var U=n(d);U!==null&&Q(O,U.startTime-H)}}var N=!1,C=-1,_=5,L=-1;function I(){return!(e.unstable_now()-L<_)}function D(){if(N){var H=e.unstable_now();L=H;var U=!0;try{e:{y=!1,v&&(v=!1,A(C),C=-1),b=!0;var F=m;try{t:{for(R(H),g=n(c);g!==null&&!(g.expirationTime>H&&I());){var Y=g.callback;if(typeof Y=="function"){g.callback=null,m=g.priorityLevel;var M=Y(g.expirationTime<=H);if(H=e.unstable_now(),typeof M=="function"){g.callback=M,R(H),U=!0;break t}g===n(c)&&r(c),R(H)}else r(c);g=n(c)}if(g!==null)U=!0;else{var V=n(d);V!==null&&Q(O,V.startTime-H),U=!1}}break e}finally{g=null,m=F,b=!1}U=void 0}}finally{U?G():N=!1}}}var G;if(typeof x=="function")G=function(){x(D)};else if(typeof MessageChannel<"u"){var $=new MessageChannel,B=$.port2;$.port1.onmessage=D,G=function(){B.postMessage(null)}}else G=function(){k(D,0)};function W(){N||(N=!0,G())}function Q(H,U){C=k(function(){H(e.unstable_now())},U)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(H){H.callback=null},e.unstable_continueExecution=function(){y||b||(y=!0,W())},e.unstable_forceFrameRate=function(H){0>H||125Y?(H.sortIndex=F,t(d,H),n(c)===null&&H===n(d)&&(v?(A(C),C=-1):v=!0,Q(O,F-Y))):(H.sortIndex=M,t(c,H),y||b||(y=!0,W())),H},e.unstable_shouldYield=I,e.unstable_wrapCallback=function(H){var U=m;return function(){var F=m;m=U;try{return H.apply(this,arguments)}finally{m=F}}}}(Zh)),Zh}var a_;function pq(){return a_||(a_=1,Xh.exports=fq()),Xh.exports}var Qh={exports:{}},wn={};/** + * @license React + * react-dom.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var o_;function gq(){if(o_)return wn;o_=1;var e=$f();function t(c){var d="https://react.dev/errors/"+c;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),Qh.exports=gq(),Qh.exports}/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var s_;function hq(){if(s_)return Ql;s_=1;var e=pq(),t=$f(),n=$z();function r(i){var l="https://react.dev/errors/"+i;if(1)":-1S||Z[h]!==ae[S]){var ye=` +`+Z[h].replace(" at new "," at ");return i.displayName&&ye.includes("")&&(ye=ye.replace("",i.displayName)),ye}while(1<=h&&0<=S);break}}}finally{W=!1,Error.prepareStackTrace=f}return(f=i?i.displayName||i.name:"")?B(f):""}function H(i){switch(i.tag){case 26:case 27:case 5:return B(i.type);case 16:return B("Lazy");case 13:return B("Suspense");case 19:return B("SuspenseList");case 0:case 15:return i=Q(i.type,!1),i;case 11:return i=Q(i.type.render,!1),i;case 1:return i=Q(i.type,!0),i;default:return""}}function U(i){try{var l="";do l+=H(i),i=i.return;while(i);return l}catch(f){return` +Error generating stack: `+f.message+` +`+f.stack}}function F(i){var l=i,f=i;if(i.alternate)for(;l.return;)l=l.return;else{i=l;do l=i,l.flags&4098&&(f=l.return),i=l.return;while(i)}return l.tag===3?f:null}function Y(i){if(i.tag===13){var l=i.memoizedState;if(l===null&&(i=i.alternate,i!==null&&(l=i.memoizedState)),l!==null)return l.dehydrated}return null}function M(i){if(F(i)!==i)throw Error(r(188))}function V(i){var l=i.alternate;if(!l){if(l=F(i),l===null)throw Error(r(188));return l!==i?null:i}for(var f=i,h=l;;){var S=f.return;if(S===null)break;var T=S.alternate;if(T===null){if(h=S.return,h!==null){f=h;continue}break}if(S.child===T.child){for(T=S.child;T;){if(T===f)return M(S),i;if(T===h)return M(S),l;T=T.sibling}throw Error(r(188))}if(f.return!==h.return)f=S,h=T;else{for(var z=!1,q=S.child;q;){if(q===f){z=!0,f=S,h=T;break}if(q===h){z=!0,h=S,f=T;break}q=q.sibling}if(!z){for(q=T.child;q;){if(q===f){z=!0,f=T,h=S;break}if(q===h){z=!0,h=T,f=S;break}q=q.sibling}if(!z)throw Error(r(189))}}if(f.alternate!==h)throw Error(r(190))}if(f.tag!==3)throw Error(r(188));return f.stateNode.current===f?i:l}function j(i){var l=i.tag;if(l===5||l===26||l===27||l===6)return i;for(i=i.child;i!==null;){if(l=j(i),l!==null)return l;i=i.sibling}return null}var P=Array.isArray,K=n.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,ee={pending:!1,data:null,method:null,action:null},le=[],X=-1;function J(i){return{current:i}}function he(i){0>X||(i.current=le[X],le[X]=null,X--)}function oe(i,l){X++,le[X]=i.current,i.current=l}var Se=J(null),we=J(null),De=J(null),Ce=J(null);function Ee(i,l){switch(oe(De,l),oe(we,i),oe(Se,null),i=l.nodeType,i){case 9:case 11:l=(l=l.documentElement)&&(l=l.namespaceURI)?CC(l):0;break;default:if(i=i===8?l.parentNode:l,l=i.tagName,i=i.namespaceURI)i=CC(i),l=_C(i,l);else switch(l){case"svg":l=1;break;case"math":l=2;break;default:l=0}}he(Se),oe(Se,l)}function te(){he(Se),he(we),he(De)}function fe(i){i.memoizedState!==null&&oe(Ce,i);var l=Se.current,f=_C(l,i.type);l!==f&&(oe(we,i),oe(Se,f))}function Te(i){we.current===i&&(he(Se),he(we)),Ce.current===i&&(he(Ce),Vl._currentValue=ee)}var be=Object.prototype.hasOwnProperty,xe=e.unstable_scheduleCallback,se=e.unstable_cancelCallback,Be=e.unstable_shouldYield,je=e.unstable_requestPaint,me=e.unstable_now,Ne=e.unstable_getCurrentPriorityLevel,ne=e.unstable_ImmediatePriority,ce=e.unstable_UserBlockingPriority,_e=e.unstable_NormalPriority,Fe=e.unstable_LowPriority,We=e.unstable_IdlePriority,St=e.log,Tt=e.unstable_setDisableYieldValue,bt=null,et=null;function At(i){if(et&&typeof et.onCommitFiberRoot=="function")try{et.onCommitFiberRoot(bt,i,void 0,(i.current.flags&128)===128)}catch{}}function st(i){if(typeof St=="function"&&Tt(i),et&&typeof et.setStrictMode=="function")try{et.setStrictMode(bt,i)}catch{}}var wt=Math.clz32?Math.clz32:zt,Ht=Math.log,pn=Math.LN2;function zt(i){return i>>>=0,i===0?32:31-(Ht(i)/pn|0)|0}var ir=128,Vr=4194304;function Jt(i){var l=i&42;if(l!==0)return l;switch(i&-i){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return i&4194176;case 4194304:case 8388608:case 16777216:case 33554432:return i&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return i}}function pa(i,l){var f=i.pendingLanes;if(f===0)return 0;var h=0,S=i.suspendedLanes,T=i.pingedLanes,z=i.warmLanes;i=i.finishedLanes!==0;var q=f&134217727;return q!==0?(f=q&~S,f!==0?h=Jt(f):(T&=q,T!==0?h=Jt(T):i||(z=q&~z,z!==0&&(h=Jt(z))))):(q=f&~S,q!==0?h=Jt(q):T!==0?h=Jt(T):i||(z=f&~z,z!==0&&(h=Jt(z)))),h===0?0:l!==0&&l!==h&&!(l&S)&&(S=h&-h,z=l&-l,S>=z||S===32&&(z&4194176)!==0)?l:h}function Xe(i,l){return(i.pendingLanes&~(i.suspendedLanes&~i.pingedLanes)&l)===0}function yt(i,l){switch(i){case 1:case 2:case 4:case 8:return l+250;case 16:case 32:case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return l+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Nt(){var i=ir;return ir<<=1,!(ir&4194176)&&(ir=128),i}function Ln(){var i=Vr;return Vr<<=1,!(Vr&62914560)&&(Vr=4194304),i}function _n(i){for(var l=[],f=0;31>f;f++)l.push(i);return l}function Mn(i,l){i.pendingLanes|=l,l!==268435456&&(i.suspendedLanes=0,i.pingedLanes=0,i.warmLanes=0)}function ga(i,l,f,h,S,T){var z=i.pendingLanes;i.pendingLanes=f,i.suspendedLanes=0,i.pingedLanes=0,i.warmLanes=0,i.expiredLanes&=f,i.entangledLanes&=f,i.errorRecoveryDisabledLanes&=f,i.shellSuspendCounter=0;var q=i.entanglements,Z=i.expirationTimes,ae=i.hiddenUpdates;for(f=z&~f;0"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ZH=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$"),_A={},NA={};function QH(i){return be.call(NA,i)?!0:be.call(_A,i)?!1:ZH.test(i)?NA[i]=!0:(_A[i]=!0,!1)}function lc(i,l,f){if(QH(l))if(f===null)i.removeAttribute(l);else{switch(typeof f){case"undefined":case"function":case"symbol":i.removeAttribute(l);return;case"boolean":var h=l.toLowerCase().slice(0,5);if(h!=="data-"&&h!=="aria-"){i.removeAttribute(l);return}}i.setAttribute(l,""+f)}}function uc(i,l,f){if(f===null)i.removeAttribute(l);else{switch(typeof f){case"undefined":case"function":case"symbol":case"boolean":i.removeAttribute(l);return}i.setAttribute(l,""+f)}}function ma(i,l,f,h){if(h===null)i.removeAttribute(f);else{switch(typeof h){case"undefined":case"function":case"symbol":case"boolean":i.removeAttribute(f);return}i.setAttributeNS(l,f,""+h)}}function sr(i){switch(typeof i){case"bigint":case"boolean":case"number":case"string":case"undefined":return i;case"object":return i;default:return""}}function OA(i){var l=i.type;return(i=i.nodeName)&&i.toLowerCase()==="input"&&(l==="checkbox"||l==="radio")}function JH(i){var l=OA(i)?"checked":"value",f=Object.getOwnPropertyDescriptor(i.constructor.prototype,l),h=""+i[l];if(!i.hasOwnProperty(l)&&typeof f<"u"&&typeof f.get=="function"&&typeof f.set=="function"){var S=f.get,T=f.set;return Object.defineProperty(i,l,{configurable:!0,get:function(){return S.call(this)},set:function(z){h=""+z,T.call(this,z)}}),Object.defineProperty(i,l,{enumerable:f.enumerable}),{getValue:function(){return h},setValue:function(z){h=""+z},stopTracking:function(){i._valueTracker=null,delete i[l]}}}}function cc(i){i._valueTracker||(i._valueTracker=JH(i))}function IA(i){if(!i)return!1;var l=i._valueTracker;if(!l)return!0;var f=l.getValue(),h="";return i&&(h=OA(i)?i.checked?"true":"false":i.value),i=h,i!==f?(l.setValue(i),!0):!1}function dc(i){if(i=i||(typeof document<"u"?document:void 0),typeof i>"u")return null;try{return i.activeElement||i.body}catch{return i.body}}var e$=/[\n"\\]/g;function lr(i){return i.replace(e$,function(l){return"\\"+l.charCodeAt(0).toString(16)+" "})}function qp(i,l,f,h,S,T,z,q){i.name="",z!=null&&typeof z!="function"&&typeof z!="symbol"&&typeof z!="boolean"?i.type=z:i.removeAttribute("type"),l!=null?z==="number"?(l===0&&i.value===""||i.value!=l)&&(i.value=""+sr(l)):i.value!==""+sr(l)&&(i.value=""+sr(l)):z!=="submit"&&z!=="reset"||i.removeAttribute("value"),l!=null?Vp(i,z,sr(l)):f!=null?Vp(i,z,sr(f)):h!=null&&i.removeAttribute("value"),S==null&&T!=null&&(i.defaultChecked=!!T),S!=null&&(i.checked=S&&typeof S!="function"&&typeof S!="symbol"),q!=null&&typeof q!="function"&&typeof q!="symbol"&&typeof q!="boolean"?i.name=""+sr(q):i.removeAttribute("name")}function DA(i,l,f,h,S,T,z,q){if(T!=null&&typeof T!="function"&&typeof T!="symbol"&&typeof T!="boolean"&&(i.type=T),l!=null||f!=null){if(!(T!=="submit"&&T!=="reset"||l!=null))return;f=f!=null?""+sr(f):"",l=l!=null?""+sr(l):f,q||l===i.value||(i.value=l),i.defaultValue=l}h=h??S,h=typeof h!="function"&&typeof h!="symbol"&&!!h,i.checked=q?i.checked:!!h,i.defaultChecked=!!h,z!=null&&typeof z!="function"&&typeof z!="symbol"&&typeof z!="boolean"&&(i.name=z)}function Vp(i,l,f){l==="number"&&dc(i.ownerDocument)===i||i.defaultValue===""+f||(i.defaultValue=""+f)}function Di(i,l,f,h){if(i=i.options,l){l={};for(var S=0;S=cl),VA=" ",WA=!1;function YA(i,l){switch(i){case"keyup":return C$.indexOf(l.keyCode)!==-1;case"keydown":return l.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function KA(i){return i=i.detail,typeof i=="object"&&"data"in i?i.data:null}var Fi=!1;function N$(i,l){switch(i){case"compositionend":return KA(l);case"keypress":return l.which!==32?null:(WA=!0,VA);case"textInput":return i=l.data,i===VA&&WA?null:i;default:return null}}function O$(i,l){if(Fi)return i==="compositionend"||!rg&&YA(i,l)?(i=jA(),pc=Qp=Ya=null,Fi=!1,i):null;switch(i){case"paste":return null;case"keypress":if(!(l.ctrlKey||l.altKey||l.metaKey)||l.ctrlKey&&l.altKey){if(l.char&&1=l)return{node:f,offset:l-i};i=h}e:{for(;f;){if(f.nextSibling){f=f.nextSibling;break e}f=f.parentNode}f=void 0}f=r1(f)}}function o1(i,l){return i&&l?i===l?!0:i&&i.nodeType===3?!1:l&&l.nodeType===3?o1(i,l.parentNode):"contains"in i?i.contains(l):i.compareDocumentPosition?!!(i.compareDocumentPosition(l)&16):!1:!1}function i1(i){i=i!=null&&i.ownerDocument!=null&&i.ownerDocument.defaultView!=null?i.ownerDocument.defaultView:window;for(var l=dc(i.document);l instanceof i.HTMLIFrameElement;){try{var f=typeof l.contentWindow.location.href=="string"}catch{f=!1}if(f)i=l.contentWindow;else break;l=dc(i.document)}return l}function ig(i){var l=i&&i.nodeName&&i.nodeName.toLowerCase();return l&&(l==="input"&&(i.type==="text"||i.type==="search"||i.type==="tel"||i.type==="url"||i.type==="password")||l==="textarea"||i.contentEditable==="true")}function B$(i,l){var f=i1(l);l=i.focusedElem;var h=i.selectionRange;if(f!==l&&l&&l.ownerDocument&&o1(l.ownerDocument.documentElement,l)){if(h!==null&&ig(l)){if(i=h.start,f=h.end,f===void 0&&(f=i),"selectionStart"in l)l.selectionStart=i,l.selectionEnd=Math.min(f,l.value.length);else if(f=(i=l.ownerDocument||document)&&i.defaultView||window,f.getSelection){f=f.getSelection();var S=l.textContent.length,T=Math.min(h.start,S);h=h.end===void 0?T:Math.min(h.end,S),!f.extend&&T>h&&(S=h,h=T,T=S),S=a1(l,T);var z=a1(l,h);S&&z&&(f.rangeCount!==1||f.anchorNode!==S.node||f.anchorOffset!==S.offset||f.focusNode!==z.node||f.focusOffset!==z.offset)&&(i=i.createRange(),i.setStart(S.node,S.offset),f.removeAllRanges(),T>h?(f.addRange(i),f.extend(z.node,z.offset)):(i.setEnd(z.node,z.offset),f.addRange(i)))}}for(i=[],f=l;f=f.parentNode;)f.nodeType===1&&i.push({element:f,left:f.scrollLeft,top:f.scrollTop});for(typeof l.focus=="function"&&l.focus(),l=0;l=document.documentMode,zi=null,sg=null,gl=null,lg=!1;function s1(i,l,f){var h=f.window===f?f.document:f.nodeType===9?f:f.ownerDocument;lg||zi==null||zi!==dc(h)||(h=zi,"selectionStart"in h&&ig(h)?h={start:h.selectionStart,end:h.selectionEnd}:(h=(h.ownerDocument&&h.ownerDocument.defaultView||window).getSelection(),h={anchorNode:h.anchorNode,anchorOffset:h.anchorOffset,focusNode:h.focusNode,focusOffset:h.focusOffset}),gl&&pl(gl,h)||(gl=h,h=ed(sg,"onSelect"),0>=z,S-=z,ba=1<<32-wt(l)+S|f<Qe?(ln=Ye,Ye=null):ln=Ye.sibling;var kt=de(ie,Ye,ue[Qe],ke);if(kt===null){Ye===null&&(Ye=ln);break}i&&Ye&&kt.alternate===null&&l(ie,Ye),re=T(kt,re,Qe),ct===null?Ge=kt:ct.sibling=kt,ct=kt,Ye=ln}if(Qe===ue.length)return f(ie,Ye),xt&&Go(ie,Qe),Ge;if(Ye===null){for(;QeQe?(ln=Ye,Ye=null):ln=Ye.sibling;var ho=de(ie,Ye,kt.value,ke);if(ho===null){Ye===null&&(Ye=ln);break}i&&Ye&&ho.alternate===null&&l(ie,Ye),re=T(ho,re,Qe),ct===null?Ge=ho:ct.sibling=ho,ct=ho,Ye=ln}if(kt.done)return f(ie,Ye),xt&&Go(ie,Qe),Ge;if(Ye===null){for(;!kt.done;Qe++,kt=ue.next())kt=Ae(ie,kt.value,ke),kt!==null&&(re=T(kt,re,Qe),ct===null?Ge=kt:ct.sibling=kt,ct=kt);return xt&&Go(ie,Qe),Ge}for(Ye=h(Ye);!kt.done;Qe++,kt=ue.next())kt=ge(Ye,ie,Qe,kt.value,ke),kt!==null&&(i&&kt.alternate!==null&&Ye.delete(kt.key===null?Qe:kt.key),re=T(kt,re,Qe),ct===null?Ge=kt:ct.sibling=kt,ct=kt);return i&&Ye.forEach(function(rq){return l(ie,rq)}),xt&&Go(ie,Qe),Ge}function Vt(ie,re,ue,ke){if(typeof ue=="object"&&ue!==null&&ue.type===c&&ue.key===null&&(ue=ue.props.children),typeof ue=="object"&&ue!==null){switch(ue.$$typeof){case s:e:{for(var Ge=ue.key;re!==null;){if(re.key===Ge){if(Ge=ue.type,Ge===c){if(re.tag===7){f(ie,re.sibling),ke=S(re,ue.props.children),ke.return=ie,ie=ke;break e}}else if(re.elementType===Ge||typeof Ge=="object"&&Ge!==null&&Ge.$$typeof===x&&k1(Ge)===re.type){f(ie,re.sibling),ke=S(re,ue.props),El(ke,ue),ke.return=ie,ie=ke;break e}f(ie,re);break}else l(ie,re);re=re.sibling}ue.type===c?(ke=Jo(ue.props.children,ie.mode,ke,ue.key),ke.return=ie,ie=ke):(ke=$c(ue.type,ue.key,ue.props,null,ie.mode,ke),El(ke,ue),ke.return=ie,ie=ke)}return z(ie);case u:e:{for(Ge=ue.key;re!==null;){if(re.key===Ge)if(re.tag===4&&re.stateNode.containerInfo===ue.containerInfo&&re.stateNode.implementation===ue.implementation){f(ie,re.sibling),ke=S(re,ue.children||[]),ke.return=ie,ie=ke;break e}else{f(ie,re);break}else l(ie,re);re=re.sibling}ke=ch(ue,ie.mode,ke),ke.return=ie,ie=ke}return z(ie);case x:return Ge=ue._init,ue=Ge(ue._payload),Vt(ie,re,ue,ke)}if(P(ue))return qe(ie,re,ue,ke);if(C(ue)){if(Ge=C(ue),typeof Ge!="function")throw Error(r(150));return ue=Ge.call(ue),rt(ie,re,ue,ke)}if(typeof ue.then=="function")return Vt(ie,re,Tc(ue),ke);if(ue.$$typeof===b)return Vt(ie,re,Uc(ie,ue),ke);Ac(ie,ue)}return typeof ue=="string"&&ue!==""||typeof ue=="number"||typeof ue=="bigint"?(ue=""+ue,re!==null&&re.tag===6?(f(ie,re.sibling),ke=S(re,ue),ke.return=ie,ie=ke):(f(ie,re),ke=uh(ue,ie.mode,ke),ke.return=ie,ie=ke),z(ie)):f(ie,re)}return function(ie,re,ue,ke){try{Sl=0;var Ge=Vt(ie,re,ue,ke);return $i=null,Ge}catch(Ye){if(Ye===yl)throw Ye;var ct=mr(29,Ye,null,ie.mode);return ct.lanes=ke,ct.return=ie,ct}finally{}}}var $o=T1(!0),A1=T1(!1),qi=J(null),Rc=J(0);function R1(i,l){i=_a,oe(Rc,i),oe(qi,l),_a=i|l.baseLanes}function mg(){oe(Rc,_a),oe(qi,qi.current)}function bg(){_a=Rc.current,he(qi),he(Rc)}var pr=J(null),Yr=null;function Xa(i){var l=i.alternate;oe(en,en.current&1),oe(pr,i),Yr===null&&(l===null||qi.current!==null||l.memoizedState!==null)&&(Yr=i)}function C1(i){if(i.tag===22){if(oe(en,en.current),oe(pr,i),Yr===null){var l=i.alternate;l!==null&&l.memoizedState!==null&&(Yr=i)}}else Za()}function Za(){oe(en,en.current),oe(pr,pr.current)}function va(i){he(pr),Yr===i&&(Yr=null),he(en)}var en=J(0);function Cc(i){for(var l=i;l!==null;){if(l.tag===13){var f=l.memoizedState;if(f!==null&&(f=f.dehydrated,f===null||f.data==="$?"||f.data==="$!"))return l}else if(l.tag===19&&l.memoizedProps.revealOrder!==void 0){if(l.flags&128)return l}else if(l.child!==null){l.child.return=l,l=l.child;continue}if(l===i)break;for(;l.sibling===null;){if(l.return===null||l.return===i)return null;l=l.return}l.sibling.return=l.return,l=l.sibling}return null}var $$=typeof AbortController<"u"?AbortController:function(){var i=[],l=this.signal={aborted:!1,addEventListener:function(f,h){i.push(h)}};this.abort=function(){l.aborted=!0,i.forEach(function(f){return f()})}},q$=e.unstable_scheduleCallback,V$=e.unstable_NormalPriority,tn={$$typeof:b,Consumer:null,Provider:null,_currentValue:null,_currentValue2:null,_threadCount:0};function yg(){return{controller:new $$,data:new Map,refCount:0}}function wl(i){i.refCount--,i.refCount===0&&q$(V$,function(){i.controller.abort()})}var xl=null,vg=0,Vi=0,Wi=null;function W$(i,l){if(xl===null){var f=xl=[];vg=0,Vi=Th(),Wi={status:"pending",value:void 0,then:function(h){f.push(h)}}}return vg++,l.then(_1,_1),l}function _1(){if(--vg===0&&xl!==null){Wi!==null&&(Wi.status="fulfilled");var i=xl;xl=null,Vi=0,Wi=null;for(var l=0;lT?T:8;var z=I.T,q={};I.T=q,Pg(i,!1,l,f);try{var Z=S(),ae=I.S;if(ae!==null&&ae(q,Z),Z!==null&&typeof Z=="object"&&typeof Z.then=="function"){var ye=Y$(Z,h);Al(i,l,ye,Qn(i))}else Al(i,l,h,Qn(i))}catch(Ae){Al(i,l,{then:function(){},status:"rejected",reason:Ae},Qn())}finally{K.p=T,I.T=z}}function J$(){}function Lg(i,l,f,h){if(i.tag!==5)throw Error(r(476));var S=iR(i).queue;oR(i,S,l,ee,f===null?J$:function(){return sR(i),f(h)})}function iR(i){var l=i.memoizedState;if(l!==null)return l;l={memoizedState:ee,baseState:ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Sa,lastRenderedState:ee},next:null};var f={};return l.next={memoizedState:f,baseState:f,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Sa,lastRenderedState:f},next:null},i.memoizedState=l,i=i.alternate,i!==null&&(i.memoizedState=l),l}function sR(i){var l=iR(i).next.queue;Al(i,l,{},Qn())}function Mg(){return En(Vl)}function lR(){return Xt().memoizedState}function uR(){return Xt().memoizedState}function e6(i){for(var l=i.return;l!==null;){switch(l.tag){case 24:case 3:var f=Qn();i=no(f);var h=ro(l,i,f);h!==null&&(On(h,l,f),_l(h,l,f)),l={cache:yg()},i.payload=l;return}l=l.return}}function t6(i,l,f){var h=Qn();f={lane:h,revertLane:0,action:f,hasEagerState:!1,eagerState:null,next:null},Fc(i)?dR(l,f):(f=dg(i,l,f,h),f!==null&&(On(f,i,h),fR(f,l,h)))}function cR(i,l,f){var h=Qn();Al(i,l,f,h)}function Al(i,l,f,h){var S={lane:h,revertLane:0,action:f,hasEagerState:!1,eagerState:null,next:null};if(Fc(i))dR(l,S);else{var T=i.alternate;if(i.lanes===0&&(T===null||T.lanes===0)&&(T=l.lastRenderedReducer,T!==null))try{var z=l.lastRenderedState,q=T(z,f);if(S.hasEagerState=!0,S.eagerState=q,Yn(q,z))return Sc(i,l,S,0),Lt===null&&vc(),!1}catch{}finally{}if(f=dg(i,l,S,h),f!==null)return On(f,i,h),fR(f,l,h),!0}return!1}function Pg(i,l,f,h){if(h={lane:2,revertLane:Th(),action:h,hasEagerState:!1,eagerState:null,next:null},Fc(i)){if(l)throw Error(r(479))}else l=dg(i,f,h,2),l!==null&&On(l,i,2)}function Fc(i){var l=i.alternate;return i===ut||l!==null&&l===ut}function dR(i,l){Yi=Nc=!0;var f=i.pending;f===null?l.next=l:(l.next=f.next,f.next=l),i.pending=l}function fR(i,l,f){if(f&4194176){var h=l.lanes;h&=i.pendingLanes,f|=h,l.lanes=f,_r(i,f)}}var Kr={readContext:En,use:Dc,useCallback:Wt,useContext:Wt,useEffect:Wt,useImperativeHandle:Wt,useLayoutEffect:Wt,useInsertionEffect:Wt,useMemo:Wt,useReducer:Wt,useRef:Wt,useState:Wt,useDebugValue:Wt,useDeferredValue:Wt,useTransition:Wt,useSyncExternalStore:Wt,useId:Wt};Kr.useCacheRefresh=Wt,Kr.useMemoCache=Wt,Kr.useHostTransitionStatus=Wt,Kr.useFormState=Wt,Kr.useActionState=Wt,Kr.useOptimistic=Wt;var Wo={readContext:En,use:Dc,useCallback:function(i,l){return Bn().memoizedState=[i,l===void 0?null:l],i},useContext:En,useEffect:Z1,useImperativeHandle:function(i,l,f){f=f!=null?f.concat([i]):null,Mc(4194308,4,eR.bind(null,l,i),f)},useLayoutEffect:function(i,l){return Mc(4194308,4,i,l)},useInsertionEffect:function(i,l){Mc(4,2,i,l)},useMemo:function(i,l){var f=Bn();l=l===void 0?null:l;var h=i();if(Vo){st(!0);try{i()}finally{st(!1)}}return f.memoizedState=[h,l],h},useReducer:function(i,l,f){var h=Bn();if(f!==void 0){var S=f(l);if(Vo){st(!0);try{f(l)}finally{st(!1)}}}else S=l;return h.memoizedState=h.baseState=S,i={pending:null,lanes:0,dispatch:null,lastRenderedReducer:i,lastRenderedState:S},h.queue=i,i=i.dispatch=t6.bind(null,ut,i),[h.memoizedState,i]},useRef:function(i){var l=Bn();return i={current:i},l.memoizedState=i},useState:function(i){i=_g(i);var l=i.queue,f=cR.bind(null,ut,l);return l.dispatch=f,[i.memoizedState,f]},useDebugValue:Ig,useDeferredValue:function(i,l){var f=Bn();return Dg(f,i,l)},useTransition:function(){var i=_g(!1);return i=oR.bind(null,ut,i.queue,!0,!1),Bn().memoizedState=i,[!1,i]},useSyncExternalStore:function(i,l,f){var h=ut,S=Bn();if(xt){if(f===void 0)throw Error(r(407));f=f()}else{if(f=l(),Lt===null)throw Error(r(349));vt&60||M1(h,l,f)}S.memoizedState=f;var T={value:f,getSnapshot:l};return S.queue=T,Z1(F1.bind(null,h,T,i),[i]),h.flags|=2048,Xi(9,P1.bind(null,h,T,f,l),{destroy:void 0},null),f},useId:function(){var i=Bn(),l=Lt.identifierPrefix;if(xt){var f=ya,h=ba;f=(h&~(1<<32-wt(h)-1)).toString(32)+f,l=":"+l+"R"+f,f=Oc++,0 title"))),mn(T,h,f),T[Sn]=i,an(T),h=T;break e;case"link":var z=BC("link","href",S).get(h+(f.href||""));if(z){for(var q=0;q<\/script>",i=i.removeChild(i.firstChild);break;case"select":i=typeof h.is=="string"?S.createElement("select",{is:h.is}):S.createElement("select"),h.multiple?i.multiple=!0:h.size&&(i.size=h.size);break;default:i=typeof h.is=="string"?S.createElement(f,{is:h.is}):S.createElement(f)}}i[Sn]=l,i[Fn]=h;e:for(S=l.child;S!==null;){if(S.tag===5||S.tag===6)i.appendChild(S.stateNode);else if(S.tag!==4&&S.tag!==27&&S.child!==null){S.child.return=S,S=S.child;continue}if(S===l)break e;for(;S.sibling===null;){if(S.return===null||S.return===l)break e;S=S.return}S.sibling.return=S.return,S=S.sibling}l.stateNode=i;e:switch(mn(i,f,h),f){case"button":case"input":case"select":case"textarea":i=!!h.autoFocus;break e;case"img":i=!0;break e;default:i=!1}i&&Ra(l)}}return Bt(l),l.flags&=-16777217,null;case 6:if(i&&l.stateNode!=null)i.memoizedProps!==h&&Ra(l);else{if(typeof h!="string"&&l.stateNode===null)throw Error(r(166));if(i=De.current,hl(l)){if(i=l.stateNode,f=l.memoizedProps,h=null,S=Nn,S!==null)switch(S.tag){case 27:case 5:h=S.memoizedProps}i[Sn]=l,i=!!(i.nodeValue===f||h!==null&&h.suppressHydrationWarning===!0||RC(i.nodeValue,f)),i||Ho(l)}else i=nd(i).createTextNode(h),i[Sn]=l,l.stateNode=i}return Bt(l),null;case 13:if(h=l.memoizedState,i===null||i.memoizedState!==null&&i.memoizedState.dehydrated!==null){if(S=hl(l),h!==null&&h.dehydrated!==null){if(i===null){if(!S)throw Error(r(318));if(S=l.memoizedState,S=S!==null?S.dehydrated:null,!S)throw Error(r(317));S[Sn]=l}else ml(),!(l.flags&128)&&(l.memoizedState=null),l.flags|=4;Bt(l),S=!1}else Or!==null&&(yh(Or),Or=null),S=!0;if(!S)return l.flags&256?(va(l),l):(va(l),null)}if(va(l),l.flags&128)return l.lanes=f,l;if(f=h!==null,i=i!==null&&i.memoizedState!==null,f){h=l.child,S=null,h.alternate!==null&&h.alternate.memoizedState!==null&&h.alternate.memoizedState.cachePool!==null&&(S=h.alternate.memoizedState.cachePool.pool);var T=null;h.memoizedState!==null&&h.memoizedState.cachePool!==null&&(T=h.memoizedState.cachePool.pool),T!==S&&(h.flags|=2048)}return f!==i&&f&&(l.child.flags|=8192),qc(l,l.updateQueue),Bt(l),null;case 4:return te(),i===null&&_h(l.stateNode.containerInfo),Bt(l),null;case 10:return xa(l.type),Bt(l),null;case 19:if(he(en),S=l.memoizedState,S===null)return Bt(l),null;if(h=(l.flags&128)!==0,T=S.rendering,T===null)if(h)Pl(S,!1);else{if(qt!==0||i!==null&&i.flags&128)for(i=l.child;i!==null;){if(T=Cc(i),T!==null){for(l.flags|=128,Pl(S,!1),i=T.updateQueue,l.updateQueue=i,qc(l,i),l.subtreeFlags=0,i=f,f=l.child;f!==null;)tC(f,i),f=f.sibling;return oe(en,en.current&1|2),l.child}i=i.sibling}S.tail!==null&&me()>Vc&&(l.flags|=128,h=!0,Pl(S,!1),l.lanes=4194304)}else{if(!h)if(i=Cc(T),i!==null){if(l.flags|=128,h=!0,i=i.updateQueue,l.updateQueue=i,qc(l,i),Pl(S,!0),S.tail===null&&S.tailMode==="hidden"&&!T.alternate&&!xt)return Bt(l),null}else 2*me()-S.renderingStartTime>Vc&&f!==536870912&&(l.flags|=128,h=!0,Pl(S,!1),l.lanes=4194304);S.isBackwards?(T.sibling=l.child,l.child=T):(i=S.last,i!==null?i.sibling=T:l.child=T,S.last=T)}return S.tail!==null?(l=S.tail,S.rendering=l,S.tail=l.sibling,S.renderingStartTime=me(),l.sibling=null,i=en.current,oe(en,h?i&1|2:i&1),l):(Bt(l),null);case 22:case 23:return va(l),bg(),h=l.memoizedState!==null,i!==null?i.memoizedState!==null!==h&&(l.flags|=8192):h&&(l.flags|=8192),h?f&536870912&&!(l.flags&128)&&(Bt(l),l.subtreeFlags&6&&(l.flags|=8192)):Bt(l),f=l.updateQueue,f!==null&&qc(l,f.retryQueue),f=null,i!==null&&i.memoizedState!==null&&i.memoizedState.cachePool!==null&&(f=i.memoizedState.cachePool.pool),h=null,l.memoizedState!==null&&l.memoizedState.cachePool!==null&&(h=l.memoizedState.cachePool.pool),h!==f&&(l.flags|=2048),i!==null&&he(qo),null;case 24:return f=null,i!==null&&(f=i.memoizedState.cache),l.memoizedState.cache!==f&&(l.flags|=2048),xa(tn),Bt(l),null;case 25:return null}throw Error(r(156,l.tag))}function l6(i,l){switch(pg(l),l.tag){case 1:return i=l.flags,i&65536?(l.flags=i&-65537|128,l):null;case 3:return xa(tn),te(),i=l.flags,i&65536&&!(i&128)?(l.flags=i&-65537|128,l):null;case 26:case 27:case 5:return Te(l),null;case 13:if(va(l),i=l.memoizedState,i!==null&&i.dehydrated!==null){if(l.alternate===null)throw Error(r(340));ml()}return i=l.flags,i&65536?(l.flags=i&-65537|128,l):null;case 19:return he(en),null;case 4:return te(),null;case 10:return xa(l.type),null;case 22:case 23:return va(l),bg(),i!==null&&he(qo),i=l.flags,i&65536?(l.flags=i&-65537|128,l):null;case 24:return xa(tn),null;case 25:return null;default:return null}}function aC(i,l){switch(pg(l),l.tag){case 3:xa(tn),te();break;case 26:case 27:case 5:Te(l);break;case 4:te();break;case 13:va(l);break;case 19:he(en);break;case 10:xa(l.type);break;case 22:case 23:va(l),bg(),i!==null&&he(qo);break;case 24:xa(tn)}}var u6={getCacheForType:function(i){var l=En(tn),f=l.data.get(i);return f===void 0&&(f=i(),l.data.set(i,f)),f}},c6=typeof WeakMap=="function"?WeakMap:Map,jt=0,Lt=null,ft=null,vt=0,Mt=0,Zn=null,Ca=!1,es=!1,dh=!1,_a=0,qt=0,lo=0,ei=0,fh=0,br=0,ts=0,Fl=null,Xr=null,ph=!1,gh=0,Vc=1/0,Wc=null,uo=null,Yc=!1,ti=null,zl=0,hh=0,mh=null,Bl=0,bh=null;function Qn(){if(jt&2&&vt!==0)return vt&-vt;if(I.T!==null){var i=Vi;return i!==0?i:Th()}return TA()}function oC(){br===0&&(br=!(vt&536870912)||xt?Nt():536870912);var i=pr.current;return i!==null&&(i.flags|=32),br}function On(i,l,f){(i===Lt&&Mt===2||i.cancelPendingCommit!==null)&&(ns(i,0),Na(i,vt,br,!1)),Mn(i,f),(!(jt&2)||i!==Lt)&&(i===Lt&&(!(jt&2)&&(ei|=f),qt===4&&Na(i,vt,br,!1)),Zr(i))}function iC(i,l,f){if(jt&6)throw Error(r(327));var h=!f&&(l&60)===0&&(l&i.expiredLanes)===0||Xe(i,l),S=h?p6(i,l):Eh(i,l,!0),T=h;do{if(S===0){es&&!h&&Na(i,l,0,!1);break}else if(S===6)Na(i,l,0,!Ca);else{if(f=i.current.alternate,T&&!d6(f)){S=Eh(i,l,!1),T=!1;continue}if(S===2){if(T=l,i.errorRecoveryDisabledLanes&T)var z=0;else z=i.pendingLanes&-536870913,z=z!==0?z:z&536870912?536870912:0;if(z!==0){l=z;e:{var q=i;S=Fl;var Z=q.current.memoizedState.isDehydrated;if(Z&&(ns(q,z).flags|=256),z=Eh(q,z,!1),z!==2){if(dh&&!Z){q.errorRecoveryDisabledLanes|=T,ei|=T,S=4;break e}T=Xr,Xr=S,T!==null&&yh(T)}S=z}if(T=!1,S!==2)continue}}if(S===1){ns(i,0),Na(i,l,0,!0);break}e:{switch(h=i,S){case 0:case 1:throw Error(r(345));case 4:if((l&4194176)===l){Na(h,l,br,!Ca);break e}break;case 2:Xr=null;break;case 3:case 5:break;default:throw Error(r(329))}if(h.finishedWork=f,h.finishedLanes=l,(l&62914560)===l&&(T=gh+300-me(),10f?32:f,I.T=null,ti===null)var T=!1;else{f=mh,mh=null;var z=ti,q=zl;if(ti=null,zl=0,jt&6)throw Error(r(331));var Z=jt;if(jt|=4,JR(z.current),XR(z,z.current,q,f),jt=Z,jl(0,!1),et&&typeof et.onPostCommitFiberRoot=="function")try{et.onPostCommitFiberRoot(bt,z)}catch{}T=!0}return T}finally{K.p=S,I.T=h,hC(i,l)}}return!1}function mC(i,l,f){l=cr(f,l),l=Bg(i.stateNode,l,2),i=ro(i,l,2),i!==null&&(Mn(i,2),Zr(i))}function Ot(i,l,f){if(i.tag===3)mC(i,i,f);else for(;l!==null;){if(l.tag===3){mC(l,i,f);break}else if(l.tag===1){var h=l.stateNode;if(typeof l.type.getDerivedStateFromError=="function"||typeof h.componentDidCatch=="function"&&(uo===null||!uo.has(h))){i=cr(f,i),f=vR(2),h=ro(l,f,2),h!==null&&(SR(f,h,l,i),Mn(h,2),Zr(h));break}}l=l.return}}function wh(i,l,f){var h=i.pingCache;if(h===null){h=i.pingCache=new c6;var S=new Set;h.set(l,S)}else S=h.get(l),S===void 0&&(S=new Set,h.set(l,S));S.has(f)||(dh=!0,S.add(f),i=m6.bind(null,i,l,f),l.then(i,i))}function m6(i,l,f){var h=i.pingCache;h!==null&&h.delete(l),i.pingedLanes|=i.suspendedLanes&f,i.warmLanes&=~f,Lt===i&&(vt&f)===f&&(qt===4||qt===3&&(vt&62914560)===vt&&300>me()-gh?!(jt&2)&&ns(i,0):fh|=f,ts===vt&&(ts=0)),Zr(i)}function bC(i,l){l===0&&(l=Ln()),i=Ka(i,l),i!==null&&(Mn(i,l),Zr(i))}function b6(i){var l=i.memoizedState,f=0;l!==null&&(f=l.retryLane),bC(i,f)}function y6(i,l){var f=0;switch(i.tag){case 13:var h=i.stateNode,S=i.memoizedState;S!==null&&(f=S.retryLane);break;case 19:h=i.stateNode;break;case 22:h=i.stateNode._retryCache;break;default:throw Error(r(314))}h!==null&&h.delete(l),bC(i,f)}function v6(i,l){return xe(i,l)}var Zc=null,os=null,xh=!1,Qc=!1,kh=!1,ni=0;function Zr(i){i!==os&&i.next===null&&(os===null?Zc=os=i:os=os.next=i),Qc=!0,xh||(xh=!0,E6(S6))}function jl(i,l){if(!kh&&Qc){kh=!0;do for(var f=!1,h=Zc;h!==null;){if(i!==0){var S=h.pendingLanes;if(S===0)var T=0;else{var z=h.suspendedLanes,q=h.pingedLanes;T=(1<<31-wt(42|i)+1)-1,T&=S&~(z&~q),T=T&201326677?T&201326677|1:T?T|2:0}T!==0&&(f=!0,SC(h,T))}else T=vt,T=pa(h,h===Lt?T:0),!(T&3)||Xe(h,T)||(f=!0,SC(h,T));h=h.next}while(f);kh=!1}}function S6(){Qc=xh=!1;var i=0;ni!==0&&(_6()&&(i=ni),ni=0);for(var l=me(),f=null,h=Zc;h!==null;){var S=h.next,T=yC(h,l);T===0?(h.next=null,f===null?Zc=S:f.next=S,S===null&&(os=f)):(f=h,(i!==0||T&3)&&(Qc=!0)),h=S}jl(i)}function yC(i,l){for(var f=i.suspendedLanes,h=i.pingedLanes,S=i.expirationTimes,T=i.pendingLanes&-62914561;0"u"?null:document;function MC(i,l,f){var h=ss;if(h&&typeof l=="string"&&l){var S=lr(l);S='link[rel="'+i+'"][href="'+S+'"]',typeof f=="string"&&(S+='[crossorigin="'+f+'"]'),LC.has(S)||(LC.add(S),i={rel:i,crossOrigin:f,href:l},h.querySelector(S)===null&&(l=h.createElement("link"),mn(l,"link",i),an(l),h.head.appendChild(l)))}}function F6(i){Oa.D(i),MC("dns-prefetch",i,null)}function z6(i,l){Oa.C(i,l),MC("preconnect",i,l)}function B6(i,l,f){Oa.L(i,l,f);var h=ss;if(h&&i&&l){var S='link[rel="preload"][as="'+lr(l)+'"]';l==="image"&&f&&f.imageSrcSet?(S+='[imagesrcset="'+lr(f.imageSrcSet)+'"]',typeof f.imageSizes=="string"&&(S+='[imagesizes="'+lr(f.imageSizes)+'"]')):S+='[href="'+lr(i)+'"]';var T=S;switch(l){case"style":T=ls(i);break;case"script":T=us(i)}yr.has(T)||(i=D({rel:"preload",href:l==="image"&&f&&f.imageSrcSet?void 0:i,as:l},f),yr.set(T,i),h.querySelector(S)!==null||l==="style"&&h.querySelector(Hl(T))||l==="script"&&h.querySelector($l(T))||(l=h.createElement("link"),mn(l,"link",i),an(l),h.head.appendChild(l)))}}function j6(i,l){Oa.m(i,l);var f=ss;if(f&&i){var h=l&&typeof l.as=="string"?l.as:"script",S='link[rel="modulepreload"][as="'+lr(h)+'"][href="'+lr(i)+'"]',T=S;switch(h){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":T=us(i)}if(!yr.has(T)&&(i=D({rel:"modulepreload",href:i},l),yr.set(T,i),f.querySelector(S)===null)){switch(h){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(f.querySelector($l(T)))return}h=f.createElement("link"),mn(h,"link",i),an(h),f.head.appendChild(h)}}}function U6(i,l,f){Oa.S(i,l,f);var h=ss;if(h&&i){var S=Oi(h).hoistableStyles,T=ls(i);l=l||"default";var z=S.get(T);if(!z){var q={loading:0,preload:null};if(z=h.querySelector(Hl(T)))q.loading=5;else{i=D({rel:"stylesheet",href:i,"data-precedence":l},f),(f=yr.get(T))&&zh(i,f);var Z=z=h.createElement("link");an(Z),mn(Z,"link",i),Z._p=new Promise(function(ae,ye){Z.onload=ae,Z.onerror=ye}),Z.addEventListener("load",function(){q.loading|=1}),Z.addEventListener("error",function(){q.loading|=2}),q.loading|=4,ad(z,l,h)}z={type:"stylesheet",instance:z,count:1,state:q},S.set(T,z)}}}function G6(i,l){Oa.X(i,l);var f=ss;if(f&&i){var h=Oi(f).hoistableScripts,S=us(i),T=h.get(S);T||(T=f.querySelector($l(S)),T||(i=D({src:i,async:!0},l),(l=yr.get(S))&&Bh(i,l),T=f.createElement("script"),an(T),mn(T,"link",i),f.head.appendChild(T)),T={type:"script",instance:T,count:1,state:null},h.set(S,T))}}function H6(i,l){Oa.M(i,l);var f=ss;if(f&&i){var h=Oi(f).hoistableScripts,S=us(i),T=h.get(S);T||(T=f.querySelector($l(S)),T||(i=D({src:i,async:!0,type:"module"},l),(l=yr.get(S))&&Bh(i,l),T=f.createElement("script"),an(T),mn(T,"link",i),f.head.appendChild(T)),T={type:"script",instance:T,count:1,state:null},h.set(S,T))}}function PC(i,l,f,h){var S=(S=De.current)?rd(S):null;if(!S)throw Error(r(446));switch(i){case"meta":case"title":return null;case"style":return typeof f.precedence=="string"&&typeof f.href=="string"?(l=ls(f.href),f=Oi(S).hoistableStyles,h=f.get(l),h||(h={type:"style",instance:null,count:0,state:null},f.set(l,h)),h):{type:"void",instance:null,count:0,state:null};case"link":if(f.rel==="stylesheet"&&typeof f.href=="string"&&typeof f.precedence=="string"){i=ls(f.href);var T=Oi(S).hoistableStyles,z=T.get(i);if(z||(S=S.ownerDocument||S,z={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},T.set(i,z),(T=S.querySelector(Hl(i)))&&!T._p&&(z.instance=T,z.state.loading=5),yr.has(i)||(f={rel:"preload",as:"style",href:f.href,crossOrigin:f.crossOrigin,integrity:f.integrity,media:f.media,hrefLang:f.hrefLang,referrerPolicy:f.referrerPolicy},yr.set(i,f),T||$6(S,i,f,z.state))),l&&h===null)throw Error(r(528,""));return z}if(l&&h!==null)throw Error(r(529,""));return null;case"script":return l=f.async,f=f.src,typeof f=="string"&&l&&typeof l!="function"&&typeof l!="symbol"?(l=us(f),f=Oi(S).hoistableScripts,h=f.get(l),h||(h={type:"script",instance:null,count:0,state:null},f.set(l,h)),h):{type:"void",instance:null,count:0,state:null};default:throw Error(r(444,i))}}function ls(i){return'href="'+lr(i)+'"'}function Hl(i){return'link[rel="stylesheet"]['+i+"]"}function FC(i){return D({},i,{"data-precedence":i.precedence,precedence:null})}function $6(i,l,f,h){i.querySelector('link[rel="preload"][as="style"]['+l+"]")?h.loading=1:(l=i.createElement("link"),h.preload=l,l.addEventListener("load",function(){return h.loading|=1}),l.addEventListener("error",function(){return h.loading|=2}),mn(l,"link",f),an(l),i.head.appendChild(l))}function us(i){return'[src="'+lr(i)+'"]'}function $l(i){return"script[async]"+i}function zC(i,l,f){if(l.count++,l.instance===null)switch(l.type){case"style":var h=i.querySelector('style[data-href~="'+lr(f.href)+'"]');if(h)return l.instance=h,an(h),h;var S=D({},f,{"data-href":f.href,"data-precedence":f.precedence,href:null,precedence:null});return h=(i.ownerDocument||i).createElement("style"),an(h),mn(h,"style",S),ad(h,f.precedence,i),l.instance=h;case"stylesheet":S=ls(f.href);var T=i.querySelector(Hl(S));if(T)return l.state.loading|=4,l.instance=T,an(T),T;h=FC(f),(S=yr.get(S))&&zh(h,S),T=(i.ownerDocument||i).createElement("link"),an(T);var z=T;return z._p=new Promise(function(q,Z){z.onload=q,z.onerror=Z}),mn(T,"link",h),l.state.loading|=4,ad(T,f.precedence,i),l.instance=T;case"script":return T=us(f.src),(S=i.querySelector($l(T)))?(l.instance=S,an(S),S):(h=f,(S=yr.get(T))&&(h=D({},f),Bh(h,S)),i=i.ownerDocument||i,S=i.createElement("script"),an(S),mn(S,"link",h),i.head.appendChild(S),l.instance=S);case"void":return null;default:throw Error(r(443,l.type))}else l.type==="stylesheet"&&!(l.state.loading&4)&&(h=l.instance,l.state.loading|=4,ad(h,f.precedence,i));return l.instance}function ad(i,l,f){for(var h=f.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),S=h.length?h[h.length-1]:null,T=S,z=0;z title"):null)}function q6(i,l,f){if(f===1||l.itemProp!=null)return!1;switch(i){case"meta":case"title":return!0;case"style":if(typeof l.precedence!="string"||typeof l.href!="string"||l.href==="")break;return!0;case"link":if(typeof l.rel!="string"||typeof l.href!="string"||l.href===""||l.onLoad||l.onError)break;switch(l.rel){case"stylesheet":return i=l.disabled,typeof l.precedence=="string"&&i==null;default:return!0}case"script":if(l.async&&typeof l.async!="function"&&typeof l.async!="symbol"&&!l.onLoad&&!l.onError&&l.src&&typeof l.src=="string")return!0}return!1}function UC(i){return!(i.type==="stylesheet"&&!(i.state.loading&3))}var ql=null;function V6(){}function W6(i,l,f){if(ql===null)throw Error(r(475));var h=ql;if(l.type==="stylesheet"&&(typeof f.media!="string"||matchMedia(f.media).matches!==!1)&&!(l.state.loading&4)){if(l.instance===null){var S=ls(f.href),T=i.querySelector(Hl(S));if(T){i=T._p,i!==null&&typeof i=="object"&&typeof i.then=="function"&&(h.count++,h=id.bind(h),i.then(h,h)),l.state.loading|=4,l.instance=T,an(T);return}T=i.ownerDocument||i,f=FC(f),(S=yr.get(S))&&zh(f,S),T=T.createElement("link"),an(T);var z=T;z._p=new Promise(function(q,Z){z.onload=q,z.onerror=Z}),mn(T,"link",f),l.instance=T}h.stylesheets===null&&(h.stylesheets=new Map),h.stylesheets.set(l,i),(i=l.state.preload)&&!(l.state.loading&3)&&(h.count++,l=id.bind(h),i.addEventListener("load",l),i.addEventListener("error",l))}}function Y6(){if(ql===null)throw Error(r(475));var i=ql;return i.stylesheets&&i.count===0&&jh(i,i.stylesheets),0"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}return e(),Kh.exports=hq(),Kh.exports}var bq=mq(),Jl={},u_;function yq(){if(u_)return Jl;u_=1,Object.defineProperty(Jl,"__esModule",{value:!0}),Jl.parse=s,Jl.serialize=d;const e=/^[\u0021-\u003A\u003C\u003E-\u007E]+$/,t=/^[\u0021-\u003A\u003C-\u007E]*$/,n=/^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i,r=/^[\u0020-\u003A\u003D-\u007E]*$/,a=Object.prototype.toString,o=(()=>{const m=function(){};return m.prototype=Object.create(null),m})();function s(m,b){const y=new o,v=m.length;if(v<2)return y;const k=(b==null?void 0:b.decode)||p;let A=0;do{const x=m.indexOf("=",A);if(x===-1)break;const R=m.indexOf(";",A),O=R===-1?v:R;if(x>O){A=m.lastIndexOf(";",x-1)+1;continue}const N=u(m,A,x),C=c(m,x,N),_=m.slice(N,C);if(y[_]===void 0){let L=u(m,x+1,O),I=c(m,O,L);const D=k(m.slice(L,I));y[_]=D}A=O+1}while(Ay;){const v=m.charCodeAt(--b);if(v!==32&&v!==9)return b+1}return y}function d(m,b,y){const v=(y==null?void 0:y.encode)||encodeURIComponent;if(!e.test(m))throw new TypeError(`argument name is invalid: ${m}`);const k=v(b);if(!t.test(k))throw new TypeError(`argument val is invalid: ${b}`);let A=m+"="+k;if(!y)return A;if(y.maxAge!==void 0){if(!Number.isInteger(y.maxAge))throw new TypeError(`option maxAge is invalid: ${y.maxAge}`);A+="; Max-Age="+y.maxAge}if(y.domain){if(!n.test(y.domain))throw new TypeError(`option domain is invalid: ${y.domain}`);A+="; Domain="+y.domain}if(y.path){if(!r.test(y.path))throw new TypeError(`option path is invalid: ${y.path}`);A+="; Path="+y.path}if(y.expires){if(!g(y.expires)||!Number.isFinite(y.expires.valueOf()))throw new TypeError(`option expires is invalid: ${y.expires}`);A+="; Expires="+y.expires.toUTCString()}if(y.httpOnly&&(A+="; HttpOnly"),y.secure&&(A+="; Secure"),y.partitioned&&(A+="; Partitioned"),y.priority)switch(typeof y.priority=="string"?y.priority.toLowerCase():void 0){case"low":A+="; Priority=Low";break;case"medium":A+="; Priority=Medium";break;case"high":A+="; Priority=High";break;default:throw new TypeError(`option priority is invalid: ${y.priority}`)}if(y.sameSite)switch(typeof y.sameSite=="string"?y.sameSite.toLowerCase():y.sameSite){case!0:case"strict":A+="; SameSite=Strict";break;case"lax":A+="; SameSite=Lax";break;case"none":A+="; SameSite=None";break;default:throw new TypeError(`option sameSite is invalid: ${y.sameSite}`)}return A}function p(m){if(m.indexOf("%")===-1)return m;try{return decodeURIComponent(m)}catch{return m}}function g(m){return a.call(m)==="[object Date]"}return Jl}yq();/** + * react-router v7.3.0 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */var c_="popstate";function vq(e={}){function t(a,o){let{pathname:s="/",search:u="",hash:c=""}=xi(a.location.hash.substring(1));return!s.startsWith("/")&&!s.startsWith(".")&&(s="/"+s),u0("",{pathname:s,search:u,hash:c},o.state&&o.state.usr||null,o.state&&o.state.key||"default")}function n(a,o){let s=a.document.querySelector("base"),u="";if(s&&s.getAttribute("href")){let c=a.location.href,d=c.indexOf("#");u=d===-1?c:c.slice(0,d)}return u+"#"+(typeof o=="string"?o:Su(o))}function r(a,o){jr(a.pathname.charAt(0)==="/",`relative pathnames are not supported in hash history.push(${JSON.stringify(o)})`)}return Eq(t,n,r,e)}function Gt(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function jr(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Sq(){return Math.random().toString(36).substring(2,10)}function d_(e,t){return{usr:e.state,key:e.key,idx:t}}function u0(e,t,n=null,r){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?xi(t):t,state:n,key:t&&t.key||r||Sq()}}function Su({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function xi(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function Eq(e,t,n,r={}){let{window:a=document.defaultView,v5Compat:o=!1}=r,s=a.history,u="POP",c=null,d=p();d==null&&(d=0,s.replaceState({...s.state,idx:d},""));function p(){return(s.state||{idx:null}).idx}function g(){u="POP";let k=p(),A=k==null?null:k-d;d=k,c&&c({action:u,location:v.location,delta:A})}function m(k,A){u="PUSH";let x=u0(v.location,k,A);n&&n(x,k),d=p()+1;let R=d_(x,d),O=v.createHref(x);try{s.pushState(R,"",O)}catch(N){if(N instanceof DOMException&&N.name==="DataCloneError")throw N;a.location.assign(O)}o&&c&&c({action:u,location:v.location,delta:1})}function b(k,A){u="REPLACE";let x=u0(v.location,k,A);n&&n(x,k),d=p();let R=d_(x,d),O=v.createHref(x);s.replaceState(R,"",O),o&&c&&c({action:u,location:v.location,delta:0})}function y(k){let A=a.location.origin!=="null"?a.location.origin:a.location.href,x=typeof k=="string"?k:Su(k);return x=x.replace(/ $/,"%20"),Gt(A,`No window.location.(origin|href) available to create URL for href: ${x}`),new URL(x,A)}let v={get action(){return u},get location(){return e(a,s)},listen(k){if(c)throw new Error("A history only accepts one active listener");return a.addEventListener(c_,g),c=k,()=>{a.removeEventListener(c_,g),c=null}},createHref(k){return t(a,k)},createURL:y,encodeLocation(k){let A=y(k);return{pathname:A.pathname,search:A.search,hash:A.hash}},push:m,replace:b,go(k){return s.go(k)}};return v}function qz(e,t,n="/"){return wq(e,t,n,!1)}function wq(e,t,n,r){let a=typeof t=="string"?xi(t):t,o=Ba(a.pathname||"/",n);if(o==null)return null;let s=Vz(e);xq(s);let u=null;for(let c=0;u==null&&c{let c={relativePath:u===void 0?o.path||"":u,caseSensitive:o.caseSensitive===!0,childrenIndex:s,route:o};c.relativePath.startsWith("/")&&(Gt(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length));let d=Fa([r,c.relativePath]),p=n.concat(c);o.children&&o.children.length>0&&(Gt(o.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${d}".`),Vz(o.children,t,p,d)),!(o.path==null&&!o.index)&&t.push({path:d,score:Nq(d,o.index),routesMeta:p})};return e.forEach((o,s)=>{var u;if(o.path===""||!((u=o.path)!=null&&u.includes("?")))a(o,s);else for(let c of Wz(o.path))a(o,s,c)}),t}function Wz(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,a=n.endsWith("?"),o=n.replace(/\?$/,"");if(r.length===0)return a?[o,""]:[o];let s=Wz(r.join("/")),u=[];return u.push(...s.map(c=>c===""?o:[o,c].join("/"))),a&&u.push(...s),u.map(c=>e.startsWith("/")&&c===""?"/":c)}function xq(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:Oq(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var kq=/^:[\w-]+$/,Tq=3,Aq=2,Rq=1,Cq=10,_q=-2,f_=e=>e==="*";function Nq(e,t){let n=e.split("/"),r=n.length;return n.some(f_)&&(r+=_q),t&&(r+=Aq),n.filter(a=>!f_(a)).reduce((a,o)=>a+(kq.test(o)?Tq:o===""?Rq:Cq),r)}function Oq(e,t){return e.length===t.length&&e.slice(0,-1).every((r,a)=>r===t[a])?e[e.length-1]-t[t.length-1]:0}function Iq(e,t,n=!1){let{routesMeta:r}=e,a={},o="/",s=[];for(let u=0;u{if(p==="*"){let y=u[m]||"";s=o.slice(0,o.length-y.length).replace(/(.)\/+$/,"$1")}const b=u[m];return g&&!b?d[p]=void 0:d[p]=(b||"").replace(/%2F/g,"/"),d},{}),pathname:o,pathnameBase:s,pattern:e}}function Dq(e,t=!1,n=!0){jr(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let r=[],a="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(s,u,c)=>(r.push({paramName:u,isOptional:c!=null}),c?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(r.push({paramName:"*"}),a+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?a+="\\/*$":e!==""&&e!=="/"&&(a+="(?:(?=\\/|$))"),[new RegExp(a,t?void 0:"i"),r]}function Lq(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return jr(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Ba(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}function Mq(e,t="/"){let{pathname:n,search:r="",hash:a=""}=typeof e=="string"?xi(e):e;return{pathname:n?n.startsWith("/")?n:Pq(n,t):t,search:Bq(r),hash:jq(a)}}function Pq(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(a=>{a===".."?n.length>1&&n.pop():a!=="."&&n.push(a)}),n.length>1?n.join("/"):"/"}function Jh(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Fq(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function Yz(e){let t=Fq(e);return t.map((n,r)=>r===t.length-1?n.pathname:n.pathnameBase)}function Kz(e,t,n,r=!1){let a;typeof e=="string"?a=xi(e):(a={...e},Gt(!a.pathname||!a.pathname.includes("?"),Jh("?","pathname","search",a)),Gt(!a.pathname||!a.pathname.includes("#"),Jh("#","pathname","hash",a)),Gt(!a.search||!a.search.includes("#"),Jh("#","search","hash",a)));let o=e===""||a.pathname==="",s=o?"/":a.pathname,u;if(s==null)u=n;else{let g=t.length-1;if(!r&&s.startsWith("..")){let m=s.split("/");for(;m[0]==="..";)m.shift(),g-=1;a.pathname=m.join("/")}u=g>=0?t[g]:"/"}let c=Mq(a,u),d=s&&s!=="/"&&s.endsWith("/"),p=(o||s===".")&&n.endsWith("/");return!c.pathname.endsWith("/")&&(d||p)&&(c.pathname+="/"),c}var Fa=e=>e.join("/").replace(/\/\/+/g,"/"),zq=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),Bq=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,jq=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function Uq(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}var Xz=["POST","PUT","PATCH","DELETE"];new Set(Xz);var Gq=["GET",...Xz];new Set(Gq);var Us=w.createContext(null);Us.displayName="DataRouter";var qf=w.createContext(null);qf.displayName="DataRouterState";var Zz=w.createContext({isTransitioning:!1});Zz.displayName="ViewTransition";var Hq=w.createContext(new Map);Hq.displayName="Fetchers";var $q=w.createContext(null);$q.displayName="Await";var sa=w.createContext(null);sa.displayName="Navigation";var zu=w.createContext(null);zu.displayName="Location";var Ha=w.createContext({outlet:null,matches:[],isDataRoute:!1});Ha.displayName="Route";var Ck=w.createContext(null);Ck.displayName="RouteError";function qq(e,{relative:t}={}){Gt(Bu(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=w.useContext(sa),{hash:a,pathname:o,search:s}=ju(e,{relative:t}),u=o;return n!=="/"&&(u=o==="/"?n:Fa([n,o])),r.createHref({pathname:u,search:s,hash:a})}function Bu(){return w.useContext(zu)!=null}function ki(){return Gt(Bu(),"useLocation() may be used only in the context of a component."),w.useContext(zu).location}var Qz="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Jz(e){w.useContext(sa).static||w.useLayoutEffect(e)}function _k(){let{isDataRoute:e}=w.useContext(Ha);return e?a9():Vq()}function Vq(){Gt(Bu(),"useNavigate() may be used only in the context of a component.");let e=w.useContext(Us),{basename:t,navigator:n}=w.useContext(sa),{matches:r}=w.useContext(Ha),{pathname:a}=ki(),o=JSON.stringify(Yz(r)),s=w.useRef(!1);return Jz(()=>{s.current=!0}),w.useCallback((c,d={})=>{if(jr(s.current,Qz),!s.current)return;if(typeof c=="number"){n.go(c);return}let p=Kz(c,JSON.parse(o),a,d.relative==="path");e==null&&t!=="/"&&(p.pathname=p.pathname==="/"?t:Fa([t,p.pathname])),(d.replace?n.replace:n.push)(p,d.state,d)},[t,n,o,a,e])}w.createContext(null);function ju(e,{relative:t}={}){let{matches:n}=w.useContext(Ha),{pathname:r}=ki(),a=JSON.stringify(Yz(n));return w.useMemo(()=>Kz(e,JSON.parse(a),r,t==="path"),[e,a,r,t])}function Wq(e,t){return eB(e,t)}function eB(e,t,n,r){var x;Gt(Bu(),"useRoutes() may be used only in the context of a component.");let{navigator:a,static:o}=w.useContext(sa),{matches:s}=w.useContext(Ha),u=s[s.length-1],c=u?u.params:{},d=u?u.pathname:"/",p=u?u.pathnameBase:"/",g=u&&u.route;{let R=g&&g.path||"";tB(d,!g||R.endsWith("*")||R.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${d}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let m=ki(),b;if(t){let R=typeof t=="string"?xi(t):t;Gt(p==="/"||((x=R.pathname)==null?void 0:x.startsWith(p)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${p}" but pathname "${R.pathname}" was given in the \`location\` prop.`),b=R}else b=m;let y=b.pathname||"/",v=y;if(p!=="/"){let R=p.replace(/^\//,"").split("/");v="/"+y.replace(/^\//,"").split("/").slice(R.length).join("/")}let k=!o&&n&&n.matches&&n.matches.length>0?n.matches:qz(e,{pathname:v});jr(g||k!=null,`No routes matched location "${b.pathname}${b.search}${b.hash}" `),jr(k==null||k[k.length-1].route.element!==void 0||k[k.length-1].route.Component!==void 0||k[k.length-1].route.lazy!==void 0,`Matched leaf route at location "${b.pathname}${b.search}${b.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let A=Qq(k&&k.map(R=>Object.assign({},R,{params:Object.assign({},c,R.params),pathname:Fa([p,a.encodeLocation?a.encodeLocation(R.pathname).pathname:R.pathname]),pathnameBase:R.pathnameBase==="/"?p:Fa([p,a.encodeLocation?a.encodeLocation(R.pathnameBase).pathname:R.pathnameBase])})),s,n,r);return t&&A?w.createElement(zu.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...b},navigationType:"POP"}},A):A}function Yq(){let e=r9(),t=Uq(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r="rgba(200,200,200, 0.5)",a={padding:"0.5rem",backgroundColor:r},o={padding:"2px 4px",backgroundColor:r},s=null;return console.error("Error handled by React Router default ErrorBoundary:",e),s=w.createElement(w.Fragment,null,w.createElement("p",null,"💿 Hey developer 👋"),w.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",w.createElement("code",{style:o},"ErrorBoundary")," or"," ",w.createElement("code",{style:o},"errorElement")," prop on your route.")),w.createElement(w.Fragment,null,w.createElement("h2",null,"Unexpected Application Error!"),w.createElement("h3",{style:{fontStyle:"italic"}},t),n?w.createElement("pre",{style:a},n):null,s)}var Kq=w.createElement(Yq,null),Xq=class extends w.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){console.error("React Router caught the following error during render",e,t)}render(){return this.state.error!==void 0?w.createElement(Ha.Provider,{value:this.props.routeContext},w.createElement(Ck.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function Zq({routeContext:e,match:t,children:n}){let r=w.useContext(Us);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),w.createElement(Ha.Provider,{value:e},n)}function Qq(e,t=[],n=null,r=null){if(e==null){if(!n)return null;if(n.errors)e=n.matches;else if(t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let a=e,o=n==null?void 0:n.errors;if(o!=null){let c=a.findIndex(d=>d.route.id&&(o==null?void 0:o[d.route.id])!==void 0);Gt(c>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(o).join(",")}`),a=a.slice(0,Math.min(a.length,c+1))}let s=!1,u=-1;if(n)for(let c=0;c=0?a=a.slice(0,u+1):a=[a[0]];break}}}return a.reduceRight((c,d,p)=>{let g,m=!1,b=null,y=null;n&&(g=o&&d.route.id?o[d.route.id]:void 0,b=d.route.errorElement||Kq,s&&(u<0&&p===0?(tB("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),m=!0,y=null):u===p&&(m=!0,y=d.route.hydrateFallbackElement||null)));let v=t.concat(a.slice(0,p+1)),k=()=>{let A;return g?A=b:m?A=y:d.route.Component?A=w.createElement(d.route.Component,null):d.route.element?A=d.route.element:A=c,w.createElement(Zq,{match:d,routeContext:{outlet:c,matches:v,isDataRoute:n!=null},children:A})};return n&&(d.route.ErrorBoundary||d.route.errorElement||p===0)?w.createElement(Xq,{location:n.location,revalidation:n.revalidation,component:b,error:g,children:k(),routeContext:{outlet:null,matches:v,isDataRoute:!0}}):k()},null)}function Nk(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Jq(e){let t=w.useContext(Us);return Gt(t,Nk(e)),t}function e9(e){let t=w.useContext(qf);return Gt(t,Nk(e)),t}function t9(e){let t=w.useContext(Ha);return Gt(t,Nk(e)),t}function Ok(e){let t=t9(e),n=t.matches[t.matches.length-1];return Gt(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function n9(){return Ok("useRouteId")}function r9(){var r;let e=w.useContext(Ck),t=e9("useRouteError"),n=Ok("useRouteError");return e!==void 0?e:(r=t.errors)==null?void 0:r[n]}function a9(){let{router:e}=Jq("useNavigate"),t=Ok("useNavigate"),n=w.useRef(!1);return Jz(()=>{n.current=!0}),w.useCallback(async(a,o={})=>{jr(n.current,Qz),n.current&&(typeof a=="number"?e.navigate(a):await e.navigate(a,{fromRouteId:t,...o}))},[e,t])}var p_={};function tB(e,t,n){!t&&!p_[e]&&(p_[e]=!0,jr(!1,n))}w.memo(o9);function o9({routes:e,future:t,state:n}){return eB(e,void 0,n,t)}function c0(e){Gt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function i9({basename:e="/",children:t=null,location:n,navigationType:r="POP",navigator:a,static:o=!1}){Gt(!Bu(),"You cannot render a inside another . You should never have more than one in your app.");let s=e.replace(/^\/*/,"/"),u=w.useMemo(()=>({basename:s,navigator:a,static:o,future:{}}),[s,a,o]);typeof n=="string"&&(n=xi(n));let{pathname:c="/",search:d="",hash:p="",state:g=null,key:m="default"}=n,b=w.useMemo(()=>{let y=Ba(c,s);return y==null?null:{location:{pathname:y,search:d,hash:p,state:g,key:m},navigationType:r}},[s,c,d,p,g,m,r]);return jr(b!=null,` is not able to match the URL "${c}${d}${p}" because it does not start with the basename, so the won't render anything.`),b==null?null:w.createElement(sa.Provider,{value:u},w.createElement(zu.Provider,{children:t,value:b}))}function s9({children:e,location:t}){return Wq(d0(e),t)}function d0(e,t=[]){let n=[];return w.Children.forEach(e,(r,a)=>{if(!w.isValidElement(r))return;let o=[...t,a];if(r.type===w.Fragment){n.push.apply(n,d0(r.props.children,o));return}Gt(r.type===c0,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),Gt(!r.props.index||!r.props.children,"An index route cannot have child routes.");let s={id:r.props.id||o.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(s.children=d0(r.props.children,o)),n.push(s)}),n}var Vd="get",Wd="application/x-www-form-urlencoded";function Vf(e){return e!=null&&typeof e.tagName=="string"}function l9(e){return Vf(e)&&e.tagName.toLowerCase()==="button"}function u9(e){return Vf(e)&&e.tagName.toLowerCase()==="form"}function c9(e){return Vf(e)&&e.tagName.toLowerCase()==="input"}function d9(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function f9(e,t){return e.button===0&&(!t||t==="_self")&&!d9(e)}var hd=null;function p9(){if(hd===null)try{new FormData(document.createElement("form"),0),hd=!1}catch{hd=!0}return hd}var g9=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function em(e){return e!=null&&!g9.has(e)?(jr(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Wd}"`),null):e}function h9(e,t){let n,r,a,o,s;if(u9(e)){let u=e.getAttribute("action");r=u?Ba(u,t):null,n=e.getAttribute("method")||Vd,a=em(e.getAttribute("enctype"))||Wd,o=new FormData(e)}else if(l9(e)||c9(e)&&(e.type==="submit"||e.type==="image")){let u=e.form;if(u==null)throw new Error('Cannot submit a + +
+ {t('documentPanel.clearDocuments.warning')} +
+
+ {t('documentPanel.clearDocuments.confirm')} +
+ +
+
+ + ) => setConfirmText(e.target.value)} + placeholder={t('documentPanel.clearDocuments.confirmPlaceholder')} + className="w-full" + /> +
+ +
+ setClearCacheOption(checked === true)} + /> + +
+
+ + + + + ) diff --git a/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx index 977c4030..5785a7d3 100644 --- a/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx +++ b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx @@ -17,7 +17,11 @@ import { uploadDocument } from '@/api/lightrag' import { UploadIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' -export default function UploadDocumentsDialog() { +interface UploadDocumentsDialogProps { + onDocumentsUploaded?: () => Promise +} + +export default function UploadDocumentsDialog({ onDocumentsUploaded }: UploadDocumentsDialogProps) { const { t } = useTranslation() const [open, setOpen] = useState(false) const [isUploading, setIsUploading] = useState(false) @@ -55,6 +59,7 @@ export default function UploadDocumentsDialog() { const handleDocumentsUpload = useCallback( async (filesToUpload: File[]) => { setIsUploading(true) + let hasSuccessfulUpload = false // Only clear errors for files that are being uploaded, keep errors for rejected files setFileErrors(prev => { @@ -101,6 +106,9 @@ export default function UploadDocumentsDialog() { ...prev, [file.name]: result.message })) + } else { + // Mark that we had at least one successful upload + hasSuccessfulUpload = true } } catch (err) { console.error(`Upload failed for ${file.name}:`, err) @@ -142,6 +150,16 @@ export default function UploadDocumentsDialog() { } else { toast.success(t('documentPanel.uploadDocuments.batch.success'), { id: toastId }) } + + // Only update if at least one file was uploaded successfully + if (hasSuccessfulUpload) { + // Refresh document list + if (onDocumentsUploaded) { + onDocumentsUploaded().catch(err => { + console.error('Error refreshing documents:', err) + }) + } + } } catch (err) { console.error('Unexpected error during upload:', err) toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }), { id: toastId }) @@ -149,7 +167,7 @@ export default function UploadDocumentsDialog() { setIsUploading(false) } }, - [setIsUploading, setProgresses, setFileErrors, t] + [setIsUploading, setProgresses, setFileErrors, t, onDocumentsUploaded] ) return ( diff --git a/lightrag_webui/src/components/graph/GraphControl.tsx b/lightrag_webui/src/components/graph/GraphControl.tsx index baa98bfe..aca8a9c4 100644 --- a/lightrag_webui/src/components/graph/GraphControl.tsx +++ b/lightrag_webui/src/components/graph/GraphControl.tsx @@ -36,6 +36,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents() const renderEdgeLabels = useSettingsStore.use.showEdgeLabel() const renderLabels = useSettingsStore.use.showNodeLabel() + const minEdgeSize = useSettingsStore.use.minEdgeSize() + const maxEdgeSize = useSettingsStore.use.maxEdgeSize() const selectedNode = useGraphStore.use.selectedNode() const focusedNode = useGraphStore.use.focusedNode() const selectedEdge = useGraphStore.use.selectedEdge() @@ -136,6 +138,51 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) registerEvents(events) }, [registerEvents, enableEdgeEvents]) + /** + * When edge size settings change, recalculate edge sizes and refresh the sigma instance + * to ensure changes take effect immediately + */ + useEffect(() => { + if (sigma && sigmaGraph) { + // Get the graph from sigma + const graph = sigma.getGraph() + + // Find min and max weight values + let minWeight = Number.MAX_SAFE_INTEGER + let maxWeight = 0 + + graph.forEachEdge(edge => { + // Get original weight (before scaling) + const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1 + if (typeof weight === 'number') { + minWeight = Math.min(minWeight, weight) + maxWeight = Math.max(maxWeight, weight) + } + }) + + // Scale edge sizes based on weight range and current min/max edge size settings + const weightRange = maxWeight - minWeight + if (weightRange > 0) { + const sizeScale = maxEdgeSize - minEdgeSize + graph.forEachEdge(edge => { + const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1 + if (typeof weight === 'number') { + const scaledSize = minEdgeSize + sizeScale * Math.pow((weight - minWeight) / weightRange, 0.5) + graph.setEdgeAttribute(edge, 'size', scaledSize) + } + }) + } else { + // If all weights are the same, use default size + graph.forEachEdge(edge => { + graph.setEdgeAttribute(edge, 'size', minEdgeSize) + }) + } + + // Refresh the sigma instance to apply changes + sigma.refresh() + } + }, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize]) + /** * When component mount or hovered node change * => Setting the sigma reducers diff --git a/lightrag_webui/src/components/graph/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx index b89c2e74..f43bd955 100644 --- a/lightrag_webui/src/components/graph/GraphLabels.tsx +++ b/lightrag_webui/src/components/graph/GraphLabels.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useEffect } from 'react' import { AsyncSelect } from '@/components/ui/AsyncSelect' import { useSettingsStore } from '@/stores/settings' import { useGraphStore } from '@/stores/graph' @@ -56,6 +56,23 @@ const GraphLabels = () => { [getSearchEngine] ) + // Validate if current queryLabel exists in allDatabaseLabels + useEffect(() => { + // Only update label when all conditions are met: + // 1. allDatabaseLabels is loaded (length > 1, as it has at least '*' by default) + // 2. Current label is not the default '*' + // 3. Current label doesn't exist in allDatabaseLabels + if ( + allDatabaseLabels.length > 1 && + label && + label !== '*' && + !allDatabaseLabels.includes(label) + ) { + console.log(`Label "${label}" not found in available labels, resetting to default`); + useSettingsStore.getState().setQueryLabel('*'); + } + }, [allDatabaseLabels, label]); + const handleRefresh = useCallback(() => { // Reset fetch status flags useGraphStore.getState().setLabelsFetchAttempted(false) diff --git a/lightrag_webui/src/components/graph/Legend.tsx b/lightrag_webui/src/components/graph/Legend.tsx new file mode 100644 index 00000000..b0b59de8 --- /dev/null +++ b/lightrag_webui/src/components/graph/Legend.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useGraphStore } from '@/stores/graph' +import { Card } from '@/components/ui/Card' +import { ScrollArea } from '@/components/ui/ScrollArea' + +interface LegendProps { + className?: string +} + +const Legend: React.FC = ({ className }) => { + const { t } = useTranslation() + const typeColorMap = useGraphStore.use.typeColorMap() + + if (!typeColorMap || typeColorMap.size === 0) { + return null + } + + return ( + +

{t('graphPanel.legend')}

+ +
+ {Array.from(typeColorMap.entries()).map(([type, color]) => ( +
+
+ + {type} + +
+ ))} +
+ + + ) +} + +export default Legend diff --git a/lightrag_webui/src/components/graph/LegendButton.tsx b/lightrag_webui/src/components/graph/LegendButton.tsx new file mode 100644 index 00000000..cf036721 --- /dev/null +++ b/lightrag_webui/src/components/graph/LegendButton.tsx @@ -0,0 +1,32 @@ +import { useCallback } from 'react' +import { BookOpenIcon } from 'lucide-react' +import Button from '@/components/ui/Button' +import { controlButtonVariant } from '@/lib/constants' +import { useSettingsStore } from '@/stores/settings' +import { useTranslation } from 'react-i18next' + +/** + * Component that toggles legend visibility. + */ +const LegendButton = () => { + const { t } = useTranslation() + const showLegend = useSettingsStore.use.showLegend() + const setShowLegend = useSettingsStore.use.setShowLegend() + + const toggleLegend = useCallback(() => { + setShowLegend(!showLegend) + }, [showLegend, setShowLegend]) + + return ( + + ) +} + +export default LegendButton diff --git a/lightrag_webui/src/components/graph/Settings.tsx b/lightrag_webui/src/components/graph/Settings.tsx index 1989a01e..af585b83 100644 --- a/lightrag_webui/src/components/graph/Settings.tsx +++ b/lightrag_webui/src/components/graph/Settings.tsx @@ -8,7 +8,7 @@ import Input from '@/components/ui/Input' import { controlButtonVariant } from '@/lib/constants' import { useSettingsStore } from '@/stores/settings' -import { SettingsIcon } from 'lucide-react' +import { SettingsIcon, Undo2 } from 'lucide-react' import { useTranslation } from 'react-i18next'; /** @@ -44,14 +44,17 @@ const LabeledNumberInput = ({ onEditFinished, label, min, - max + max, + defaultValue }: { value: number onEditFinished: (value: number) => void label: string min: number max?: number + defaultValue?: number }) => { + const { t } = useTranslation(); const [currentValue, setCurrentValue] = useState(value) const onValueChange = useCallback( @@ -81,6 +84,13 @@ const LabeledNumberInput = ({ } }, [value, currentValue, onEditFinished]) + const handleReset = useCallback(() => { + if (defaultValue !== undefined && value !== defaultValue) { + setCurrentValue(defaultValue) + onEditFinished(defaultValue) + } + }, [defaultValue, value, onEditFinished]) + return (
- { - if (e.key === 'Enter') { - onBlur() - } - }} - /> +
+ { + if (e.key === 'Enter') { + onBlur() + } + }} + /> + {defaultValue !== undefined && ( + + )} +
) } @@ -120,8 +144,10 @@ export default function Settings() { const enableNodeDrag = useSettingsStore.use.enableNodeDrag() const enableHideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges() const showEdgeLabel = useSettingsStore.use.showEdgeLabel() + const minEdgeSize = useSettingsStore.use.minEdgeSize() + const maxEdgeSize = useSettingsStore.use.maxEdgeSize() const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth() - const graphMinDegree = useSettingsStore.use.graphMinDegree() + const graphMaxNodes = useSettingsStore.use.graphMaxNodes() const graphLayoutMaxIterations = useSettingsStore.use.graphLayoutMaxIterations() const enableHealthCheck = useSettingsStore.use.enableHealthCheck() @@ -180,15 +206,14 @@ export default function Settings() { }, 300) }, []) - const setGraphMinDegree = useCallback((degree: number) => { - if (degree < 0) return - useSettingsStore.setState({ graphMinDegree: degree }) + const setGraphMaxNodes = useCallback((nodes: number) => { + if (nodes < 1 || nodes > 1000) return + useSettingsStore.setState({ graphMaxNodes: nodes }) const currentLabel = useSettingsStore.getState().queryLabel useSettingsStore.getState().setQueryLabel('') setTimeout(() => { useSettingsStore.getState().setQueryLabel(currentLabel) }, 300) - }, []) const setGraphLayoutMaxIterations = useCallback((iterations: number) => { @@ -269,24 +294,75 @@ export default function Settings() { label={t('graphPanel.sideBar.settings.edgeEvents')} /> +
+ +
+ { + const newValue = Number(e.target.value); + if (!isNaN(newValue) && newValue >= 1 && newValue <= maxEdgeSize) { + useSettingsStore.setState({ minEdgeSize: newValue }); + } + }} + className="h-6 w-16 min-w-0 pr-1" + min={1} + max={Math.min(maxEdgeSize, 10)} + /> + - +
+ { + const newValue = Number(e.target.value); + if (!isNaN(newValue) && newValue >= minEdgeSize && newValue >= 1 && newValue <= 10) { + useSettingsStore.setState({ maxEdgeSize: newValue }); + } + }} + className="h-6 w-16 min-w-0 pr-1" + min={minEdgeSize} + max={10} + /> + +
+
+
+ diff --git a/lightrag_webui/src/components/graph/SettingsDisplay.tsx b/lightrag_webui/src/components/graph/SettingsDisplay.tsx index dec44c11..93fc0e01 100644 --- a/lightrag_webui/src/components/graph/SettingsDisplay.tsx +++ b/lightrag_webui/src/components/graph/SettingsDisplay.tsx @@ -8,12 +8,12 @@ import { useTranslation } from 'react-i18next' const SettingsDisplay = () => { const { t } = useTranslation() const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth() - const graphMinDegree = useSettingsStore.use.graphMinDegree() + const graphMaxNodes = useSettingsStore.use.graphMaxNodes() return (
{t('graphPanel.sideBar.settings.depth')}: {graphQueryMaxDepth}
-
{t('graphPanel.sideBar.settings.degree')}: {graphMinDegree}
+
{t('graphPanel.sideBar.settings.max')}: {graphMaxNodes}
) } diff --git a/lightrag_webui/src/components/status/StatusCard.tsx b/lightrag_webui/src/components/status/StatusCard.tsx index e67cbd30..c9e64db9 100644 --- a/lightrag_webui/src/components/status/StatusCard.tsx +++ b/lightrag_webui/src/components/status/StatusCard.tsx @@ -4,14 +4,14 @@ import { useTranslation } from 'react-i18next' const StatusCard = ({ status }: { status: LightragStatus | null }) => { const { t } = useTranslation() if (!status) { - return
{t('graphPanel.statusCard.unavailable')}
+ return
{t('graphPanel.statusCard.unavailable')}
} return ( -
+

{t('graphPanel.statusCard.storageInfo')}

-
+
{t('graphPanel.statusCard.workingDirectory')}: {status.working_directory} {t('graphPanel.statusCard.inputDirectory')}: @@ -21,7 +21,7 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => {

{t('graphPanel.statusCard.llmConfig')}

-
+
{t('graphPanel.statusCard.llmBinding')}: {status.configuration.llm_binding} {t('graphPanel.statusCard.llmBindingHost')}: @@ -35,7 +35,7 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => {

{t('graphPanel.statusCard.embeddingConfig')}

-
+
{t('graphPanel.statusCard.embeddingBinding')}: {status.configuration.embedding_binding} {t('graphPanel.statusCard.embeddingBindingHost')}: @@ -47,7 +47,7 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => {

{t('graphPanel.statusCard.storageConfig')}

-
+
{t('graphPanel.statusCard.kvStorage')}: {status.configuration.kv_storage} {t('graphPanel.statusCard.docStatusStorage')}: diff --git a/lightrag_webui/src/components/status/StatusDialog.tsx b/lightrag_webui/src/components/status/StatusDialog.tsx new file mode 100644 index 00000000..48eaa4f7 --- /dev/null +++ b/lightrag_webui/src/components/status/StatusDialog.tsx @@ -0,0 +1,32 @@ +import { LightragStatus } from '@/api/lightrag' +import { useTranslation } from 'react-i18next' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/Dialog' +import StatusCard from './StatusCard' + +interface StatusDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + status: LightragStatus | null +} + +const StatusDialog = ({ open, onOpenChange, status }: StatusDialogProps) => { + const { t } = useTranslation() + + return ( + + + + {t('graphPanel.statusDialog.title')} + + + + + ) +} + +export default StatusDialog diff --git a/lightrag_webui/src/components/status/StatusIndicator.tsx b/lightrag_webui/src/components/status/StatusIndicator.tsx index 263bb99e..5a9fc751 100644 --- a/lightrag_webui/src/components/status/StatusIndicator.tsx +++ b/lightrag_webui/src/components/status/StatusIndicator.tsx @@ -1,8 +1,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/status/StatusCard' +import StatusDialog from './StatusDialog' import { useTranslation } from 'react-i18next' const StatusIndicator = () => { @@ -11,6 +10,7 @@ const StatusIndicator = () => { const lastCheckTime = useBackendState.use.lastCheckTime() const status = useBackendState.use.status() const [animate, setAnimate] = useState(false) + const [dialogOpen, setDialogOpen] = useState(false) // listen to health change useEffect(() => { @@ -21,28 +21,30 @@ const StatusIndicator = () => { return (
- - -
-
- - {health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')} - -
- - - - - +
setDialogOpen(true)} + > +
+ + {health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')} + +
+ +
) } diff --git a/lightrag_webui/src/components/ui/Checkbox.tsx b/lightrag_webui/src/components/ui/Checkbox.tsx index 36ebe6e0..c9d4fafe 100644 --- a/lightrag_webui/src/components/ui/Checkbox.tsx +++ b/lightrag_webui/src/components/ui/Checkbox.tsx @@ -11,7 +11,7 @@ const Checkbox = React.forwardRef< >( { // Check if file_path exists and is a non-empty string if (!doc.file_path || typeof doc.file_path !== 'string' || doc.file_path.trim() === '') { @@ -148,6 +150,10 @@ export default function DocumentManager() { const [sortField, setSortField] = useState('updated_at') const [sortDirection, setSortDirection] = useState('desc') + // State for document status filter + const [statusFilter, setStatusFilter] = useState('all'); + + // Handle sort column click const handleSort = (field: SortField) => { if (sortField === field) { @@ -161,7 +167,7 @@ export default function DocumentManager() { } // Sort documents based on current sort field and direction - const sortDocuments = (documents: DocStatusResponse[]) => { + const sortDocuments = useCallback((documents: DocStatusResponse[]) => { return [...documents].sort((a, b) => { let valueA, valueB; @@ -188,7 +194,50 @@ export default function DocumentManager() { return sortMultiplier * (valueA > valueB ? 1 : valueA < valueB ? -1 : 0); } }); - } + }, [sortField, sortDirection, showFileName]); + + const filteredAndSortedDocs = useMemo(() => { + if (!docs) return null; + + let filteredDocs = { ...docs }; + + if (statusFilter !== 'all') { + filteredDocs = { + ...docs, + statuses: { + pending: [], + processing: [], + processed: [], + failed: [], + [statusFilter]: docs.statuses[statusFilter] || [] + } + }; + } + + if (!sortField || !sortDirection) return filteredDocs; + + const sortedStatuses = Object.entries(filteredDocs.statuses).reduce((acc, [status, documents]) => { + const sortedDocuments = sortDocuments(documents); + acc[status as DocStatus] = sortedDocuments; + return acc; + }, {} as DocsStatusesResponse['statuses']); + + return { ...filteredDocs, statuses: sortedStatuses }; + }, [docs, sortField, sortDirection, statusFilter, sortDocuments]); + + // Calculate document counts for each status + const documentCounts = useMemo(() => { + if (!docs) return { all: 0 } as Record; + + const counts: Record = { all: 0 }; + + Object.entries(docs.statuses).forEach(([status, documents]) => { + counts[status as DocStatus] = documents.length; + counts.all += documents.length; + }); + + return counts; + }, [docs]); // Store previous status counts const prevStatusCounts = useRef({ @@ -386,8 +435,8 @@ export default function DocumentManager() {
- - + +
{t('documentPanel.documentManager.uploadedTitle')} +
+ +
+ + + + + +
+
{t('documentPanel.documentManager.fileNameLabel')}
diff --git a/lightrag_webui/src/features/GraphViewer.tsx b/lightrag_webui/src/features/GraphViewer.tsx index 9d46af55..754101a7 100644 --- a/lightrag_webui/src/features/GraphViewer.tsx +++ b/lightrag_webui/src/features/GraphViewer.tsx @@ -18,6 +18,8 @@ import GraphSearch from '@/components/graph/GraphSearch' import GraphLabels from '@/components/graph/GraphLabels' import PropertiesView from '@/components/graph/PropertiesView' import SettingsDisplay from '@/components/graph/SettingsDisplay' +import Legend from '@/components/graph/Legend' +import LegendButton from '@/components/graph/LegendButton' import { useSettingsStore } from '@/stores/settings' import { useGraphStore } from '@/stores/graph' @@ -116,6 +118,7 @@ const GraphViewer = () => { const showPropertyPanel = useSettingsStore.use.showPropertyPanel() const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() const enableNodeDrag = useSettingsStore.use.enableNodeDrag() + const showLegend = useSettingsStore.use.showLegend() // Initialize sigma settings once on component mount // All dynamic settings will be updated in GraphControl using useSetSettings @@ -195,6 +198,7 @@ const GraphViewer = () => { + {/* */}
@@ -205,6 +209,12 @@ const GraphViewer = () => {
)} + {showLegend && ( +
+ +
+ )} + {/*
*/} diff --git a/lightrag_webui/src/features/LoginPage.tsx b/lightrag_webui/src/features/LoginPage.tsx index 847a4c9e..fc60370c 100644 --- a/lightrag_webui/src/features/LoginPage.tsx +++ b/lightrag_webui/src/features/LoginPage.tsx @@ -51,7 +51,7 @@ const LoginPage = () => { if (!status.auth_configured && status.access_token) { // If auth is not configured, use the guest token and redirect - login(status.access_token, true, status.core_version, status.api_version) + login(status.access_token, true, status.core_version, status.api_version, status.webui_title || null, status.webui_description || null) if (status.message) { toast.info(status.message) } @@ -96,7 +96,7 @@ const LoginPage = () => { // Check authentication mode const isGuestMode = response.auth_mode === 'disabled' - login(response.access_token, isGuestMode, response.core_version, response.api_version) + login(response.access_token, isGuestMode, response.core_version, response.api_version, response.webui_title || null, response.webui_description || null) // Set session flag for version check if (response.core_version || response.api_version) { diff --git a/lightrag_webui/src/features/SiteHeader.tsx b/lightrag_webui/src/features/SiteHeader.tsx index 8077d390..4881e4b6 100644 --- a/lightrag_webui/src/features/SiteHeader.tsx +++ b/lightrag_webui/src/features/SiteHeader.tsx @@ -8,6 +8,7 @@ import { cn } from '@/lib/utils' import { useTranslation } from 'react-i18next' import { navigationService } from '@/services/navigation' import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip' interface NavigationTabProps { value: string @@ -55,7 +56,7 @@ function TabsNavigation() { export default function SiteHeader() { const { t } = useTranslation() - const { isGuestMode, coreVersion, apiVersion, username } = useAuthStore() + const { isGuestMode, coreVersion, apiVersion, username, webuiTitle, webuiDescription } = useAuthStore() const versionDisplay = (coreVersion && apiVersion) ? `${coreVersion}/${apiVersion}` @@ -67,17 +68,31 @@ export default function SiteHeader() { return (
-
+
+ {webuiTitle && ( +
+ | + + + + + {webuiTitle} + + + {webuiDescription && ( + + {webuiDescription} + + )} + + +
+ )}
@@ -91,6 +106,11 @@ export default function SiteHeader() {