Merge remote-tracking branch 'origin/main' into api_improvment
# Conflicts: # lightrag/api/lightrag_server.py
This commit is contained in:
20
.env.example
20
.env.example
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
### Logging level
|
### Logging level
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
|
VERBOSE=False
|
||||||
|
|
||||||
### Optional Timeout
|
### Optional Timeout
|
||||||
TIMEOUT=300
|
TIMEOUT=300
|
||||||
@@ -27,14 +28,21 @@ TIMEOUT=300
|
|||||||
|
|
||||||
### RAG Configuration
|
### RAG Configuration
|
||||||
MAX_ASYNC=4
|
MAX_ASYNC=4
|
||||||
MAX_TOKENS=32768
|
|
||||||
EMBEDDING_DIM=1024
|
EMBEDDING_DIM=1024
|
||||||
MAX_EMBED_TOKENS=8192
|
MAX_EMBED_TOKENS=8192
|
||||||
#HISTORY_TURNS=3
|
### Settings relative to query
|
||||||
#CHUNK_SIZE=1200
|
HISTORY_TURNS=3
|
||||||
#CHUNK_OVERLAP_SIZE=100
|
COSINE_THRESHOLD=0.2
|
||||||
#COSINE_THRESHOLD=0.2
|
TOP_K=60
|
||||||
#TOP_K=60
|
MAX_TOKEN_TEXT_CHUNK=4000
|
||||||
|
MAX_TOKEN_RELATION_DESC=4000
|
||||||
|
MAX_TOKEN_ENTITY_DESC=4000
|
||||||
|
### Settings relative to indexing
|
||||||
|
CHUNK_SIZE=1200
|
||||||
|
CHUNK_OVERLAP_SIZE=100
|
||||||
|
MAX_TOKENS=32768
|
||||||
|
MAX_TOKEN_SUMMARY=500
|
||||||
|
SUMMARY_LANGUAGE=English
|
||||||
|
|
||||||
### LLM Configuration (Use valid host. For local services, you can use host.docker.internal)
|
### LLM Configuration (Use valid host. For local services, you can use host.docker.internal)
|
||||||
### Ollama example
|
### Ollama example
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,7 +5,7 @@ __pycache__/
|
|||||||
.eggs/
|
.eggs/
|
||||||
*.tgz
|
*.tgz
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.ini # Remove config.ini from repo
|
*.ini
|
||||||
|
|
||||||
# Virtual Environment
|
# Virtual Environment
|
||||||
.venv/
|
.venv/
|
||||||
@@ -31,6 +31,7 @@ log/
|
|||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.gradio/
|
.gradio/
|
||||||
|
.history/
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
# IDE / Editor Files
|
# IDE / Editor Files
|
||||||
|
@@ -222,6 +222,7 @@ You can select storage implementation by enviroment variables or command line a
|
|||||||
| --max-embed-tokens | 8192 | Maximum embedding token size |
|
| --max-embed-tokens | 8192 | Maximum embedding token size |
|
||||||
| --timeout | None | Timeout in seconds (useful when using slow AI). Use None for infinite timeout |
|
| --timeout | None | Timeout in seconds (useful when using slow AI). Use None for infinite timeout |
|
||||||
| --log-level | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
|
| --log-level | INFO | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
|
||||||
|
| --verbose | False | Verbose debug output (True, Flase) |
|
||||||
| --key | None | API key for authentication. Protects lightrag server against unauthorized access |
|
| --key | None | API key for authentication. Protects lightrag server against unauthorized access |
|
||||||
| --ssl | False | Enable HTTPS |
|
| --ssl | False | Enable HTTPS |
|
||||||
| --ssl-certfile | None | Path to SSL certificate file (required if --ssl is enabled) |
|
| --ssl-certfile | None | Path to SSL certificate file (required if --ssl is enabled) |
|
||||||
|
@@ -61,7 +61,10 @@ from ..kg.tidb_impl import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv(override=True)
|
try:
|
||||||
|
load_dotenv(override=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to load .env file: {e}")
|
||||||
|
|
||||||
# Initialize config parser
|
# Initialize config parser
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
@@ -131,8 +134,8 @@ def get_env_value(env_key: str, default: Any, value_type: type = str) -> Any:
|
|||||||
if value is None:
|
if value is None:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
if isinstance(value_type, bool):
|
if value_type is bool:
|
||||||
return value.lower() in ("true", "1", "yes")
|
return value.lower() in ("true", "1", "yes", "t", "on")
|
||||||
try:
|
try:
|
||||||
return value_type(value)
|
return value_type(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -234,6 +237,8 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
|||||||
ASCIIColors.yellow(f"{ollama_server_infos.LIGHTRAG_MODEL}")
|
ASCIIColors.yellow(f"{ollama_server_infos.LIGHTRAG_MODEL}")
|
||||||
ASCIIColors.white(" ├─ Log Level: ", end="")
|
ASCIIColors.white(" ├─ Log Level: ", end="")
|
||||||
ASCIIColors.yellow(f"{args.log_level}")
|
ASCIIColors.yellow(f"{args.log_level}")
|
||||||
|
ASCIIColors.white(" ├─ Verbose Debug: ", end="")
|
||||||
|
ASCIIColors.yellow(f"{args.verbose}")
|
||||||
ASCIIColors.white(" └─ Timeout: ", end="")
|
ASCIIColors.white(" └─ Timeout: ", end="")
|
||||||
ASCIIColors.yellow(f"{args.timeout if args.timeout else 'None (infinite)'}")
|
ASCIIColors.yellow(f"{args.timeout if args.timeout else 'None (infinite)'}")
|
||||||
|
|
||||||
@@ -252,10 +257,8 @@ def display_splash_screen(args: argparse.Namespace) -> None:
|
|||||||
ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/docs")
|
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.yellow(f"{protocol}://localhost:{args.port}/redoc")
|
||||||
ASCIIColors.white(" ├─ WebUI (local): ", end="")
|
ASCIIColors.white(" └─ WebUI (local): ", end="")
|
||||||
ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/webui")
|
ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/webui")
|
||||||
ASCIIColors.white(" └─ Graph Viewer (local): ", end="")
|
|
||||||
ASCIIColors.yellow(f"{protocol}://localhost:{args.port}/graph-viewer")
|
|
||||||
|
|
||||||
ASCIIColors.yellow("\n📝 Note:")
|
ASCIIColors.yellow("\n📝 Note:")
|
||||||
ASCIIColors.white(""" Since the server is running on 0.0.0.0:
|
ASCIIColors.white(""" Since the server is running on 0.0.0.0:
|
||||||
@@ -565,6 +568,13 @@ def parse_args() -> argparse.Namespace:
|
|||||||
help="Prefix of the namespace",
|
help="Prefix of the namespace",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
type=bool,
|
||||||
|
default=get_env_value("VERBOSE", False, bool),
|
||||||
|
help="Verbose debug output(default: from env or false)",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# conver relative path to absolute path
|
# conver relative path to absolute path
|
||||||
@@ -776,6 +786,23 @@ class InsertResponse(BaseModel):
|
|||||||
message: str = Field(description="Message describing the operation result")
|
message: str = Field(description="Message describing the operation result")
|
||||||
|
|
||||||
|
|
||||||
|
class DocStatusResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
content_summary: str
|
||||||
|
content_length: int
|
||||||
|
status: DocStatus
|
||||||
|
created_at: str
|
||||||
|
updated_at: str
|
||||||
|
chunks_count: Optional[int] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
metadata: Optional[dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DocsStatusesResponse(BaseModel):
|
||||||
|
statuses: Dict[DocStatus, List[DocStatusResponse]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_api_key_dependency(api_key: Optional[str]):
|
def get_api_key_dependency(api_key: Optional[str]):
|
||||||
if not api_key:
|
if not api_key:
|
||||||
# If no API key is configured, return a dummy dependency that always succeeds
|
# If no API key is configured, return a dummy dependency that always succeeds
|
||||||
@@ -809,6 +836,11 @@ temp_prefix = "__tmp_" # prefix for temporary files
|
|||||||
|
|
||||||
|
|
||||||
def create_app(args):
|
def create_app(args):
|
||||||
|
# Initialize verbose debug setting
|
||||||
|
from lightrag.utils import set_verbose_debug
|
||||||
|
|
||||||
|
set_verbose_debug(args.verbose)
|
||||||
|
|
||||||
global global_top_k
|
global global_top_k
|
||||||
global_top_k = args.top_k # save top_k from args
|
global_top_k = args.top_k # save top_k from args
|
||||||
|
|
||||||
@@ -1806,20 +1838,57 @@ def create_app(args):
|
|||||||
app.include_router(ollama_api.router, prefix="/api")
|
app.include_router(ollama_api.router, prefix="/api")
|
||||||
|
|
||||||
@app.get("/documents", dependencies=[Depends(optional_api_key)])
|
@app.get("/documents", dependencies=[Depends(optional_api_key)])
|
||||||
async def documents():
|
async def documents() -> DocsStatusesResponse:
|
||||||
"""Get current system status"""
|
"""
|
||||||
return doc_manager.indexed_files
|
Get documents statuses
|
||||||
|
Returns:
|
||||||
|
DocsStatusesResponse: A response object containing a dictionary where keys are DocStatus
|
||||||
|
and values are lists of DocStatusResponse objects representing documents in each status category.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statuses = (
|
||||||
|
DocStatus.PENDING,
|
||||||
|
DocStatus.PROCESSING,
|
||||||
|
DocStatus.PROCESSED,
|
||||||
|
DocStatus.FAILED,
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks = [rag.get_docs_by_status(status) for status in statuses]
|
||||||
|
results: List[Dict[str, DocProcessingStatus]] = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
response = DocsStatusesResponse()
|
||||||
|
|
||||||
|
for idx, result in enumerate(results):
|
||||||
|
status = statuses[idx]
|
||||||
|
for doc_id, doc_status in result.items():
|
||||||
|
if status not in response.statuses:
|
||||||
|
response.statuses[status] = []
|
||||||
|
response.statuses[status].append(
|
||||||
|
DocStatusResponse(
|
||||||
|
id=doc_id,
|
||||||
|
content_summary=doc_status.content_summary,
|
||||||
|
content_length=doc_status.content_length,
|
||||||
|
status=doc_status.status,
|
||||||
|
created_at=doc_status.created_at,
|
||||||
|
updated_at=doc_status.updated_at,
|
||||||
|
chunks_count=doc_status.chunks_count,
|
||||||
|
error=doc_status.error,
|
||||||
|
metadata=doc_status.metadata,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error GET /documents: {str(e)}")
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
@app.get("/health", dependencies=[Depends(optional_api_key)])
|
||||||
async def get_status():
|
async def get_status():
|
||||||
"""Get current system status"""
|
"""Get current system status"""
|
||||||
files = doc_manager.scan_directory()
|
|
||||||
return {
|
return {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"working_directory": str(args.working_dir),
|
"working_directory": str(args.working_dir),
|
||||||
"input_directory": str(args.input_dir),
|
"input_directory": str(args.input_dir),
|
||||||
"indexed_files": [str(f) for f in files],
|
|
||||||
"indexed_files_count": len(files),
|
|
||||||
"configuration": {
|
"configuration": {
|
||||||
# LLM configuration binding/host address (if applicable)/model (if applicable)
|
# LLM configuration binding/host address (if applicable)/model (if applicable)
|
||||||
"llm_binding": args.llm_binding,
|
"llm_binding": args.llm_binding,
|
||||||
@@ -1838,17 +1907,9 @@ def create_app(args):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Webui mount webui/index.html
|
# Webui mount webui/index.html
|
||||||
webui_dir = Path(__file__).parent / "webui"
|
static_dir = Path(__file__).parent / "webui"
|
||||||
app.mount(
|
|
||||||
"/graph-viewer",
|
|
||||||
StaticFiles(directory=webui_dir, html=True),
|
|
||||||
name="webui",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Serve the static files
|
|
||||||
static_dir = Path(__file__).parent / "static"
|
|
||||||
static_dir.mkdir(exist_ok=True)
|
static_dir.mkdir(exist_ok=True)
|
||||||
app.mount("/webui", StaticFiles(directory=static_dir, html=True), name="static")
|
app.mount("/webui", StaticFiles(directory=static_dir, html=True), name="webui")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ from fastapi.responses import StreamingResponse
|
|||||||
import asyncio
|
import asyncio
|
||||||
from ascii_colors import trace_exception
|
from ascii_colors import trace_exception
|
||||||
from lightrag import LightRAG, QueryParam
|
from lightrag import LightRAG, QueryParam
|
||||||
|
from lightrag.utils import encode_string_by_tiktoken
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
@@ -111,18 +112,9 @@ class OllamaTagResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
def estimate_tokens(text: str) -> int:
|
def estimate_tokens(text: str) -> int:
|
||||||
"""Estimate the number of tokens in text
|
"""Estimate the number of tokens in text using tiktoken"""
|
||||||
Chinese characters: approximately 1.5 tokens per character
|
tokens = encode_string_by_tiktoken(text)
|
||||||
English characters: approximately 0.25 tokens per character
|
return len(tokens)
|
||||||
"""
|
|
||||||
# Use regex to match Chinese and non-Chinese characters separately
|
|
||||||
chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", text))
|
|
||||||
non_chinese_chars = len(re.findall(r"[^\u4e00-\u9fff]", text))
|
|
||||||
|
|
||||||
# Calculate estimated token count
|
|
||||||
tokens = chinese_chars * 1.5 + non_chinese_chars * 0.25
|
|
||||||
|
|
||||||
return int(tokens)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_query_mode(query: str) -> tuple[str, SearchMode]:
|
def parse_query_mode(query: str) -> tuple[str, SearchMode]:
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
# LightRag Webui
|
|
||||||
A simple webui to interact with the lightrag datalake
|
|
Binary file not shown.
Before Width: | Height: | Size: 734 KiB |
@@ -1,104 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>LightRAG Interface</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
||||||
<style>
|
|
||||||
.fade-in {
|
|
||||||
animation: fadeIn 0.3s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.spin {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-in {
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from { transform: translateX(-100%); }
|
|
||||||
to { transform: translateX(0); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50">
|
|
||||||
<div class="flex h-screen">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<div class="w-64 bg-white shadow-lg">
|
|
||||||
<div class="p-4">
|
|
||||||
<h1 class="text-xl font-bold text-gray-800 mb-6">LightRAG</h1>
|
|
||||||
<nav class="space-y-2">
|
|
||||||
<a href="#" class="nav-item" data-page="file-manager">
|
|
||||||
<div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
|
||||||
</svg>
|
|
||||||
File Manager
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="nav-item" data-page="query">
|
|
||||||
<div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
||||||
</svg>
|
|
||||||
Query Database
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="nav-item" data-page="knowledge-graph">
|
|
||||||
<div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
||||||
</svg>
|
|
||||||
Knowledge Graph
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="nav-item" data-page="status">
|
|
||||||
<div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
|
||||||
</svg>
|
|
||||||
Status
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="nav-item" data-page="settings">
|
|
||||||
<div class="flex items-center p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
||||||
</svg>
|
|
||||||
Settings
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="flex-1 overflow-auto p-6">
|
|
||||||
<div id="content" class="fade-in"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast Notification -->
|
|
||||||
<div id="toast" class="fixed bottom-4 right-4 hidden">
|
|
||||||
<div class="bg-gray-800 text-white px-6 py-3 rounded-lg shadow-lg"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="./js/api.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@@ -1,408 +0,0 @@
|
|||||||
// State management
|
|
||||||
const state = {
|
|
||||||
apiKey: localStorage.getItem('apiKey') || '',
|
|
||||||
files: [],
|
|
||||||
indexedFiles: [],
|
|
||||||
currentPage: 'file-manager'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Utility functions
|
|
||||||
const showToast = (message, duration = 3000) => {
|
|
||||||
const toast = document.getElementById('toast');
|
|
||||||
toast.querySelector('div').textContent = message;
|
|
||||||
toast.classList.remove('hidden');
|
|
||||||
setTimeout(() => toast.classList.add('hidden'), duration);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchWithAuth = async (url, options = {}) => {
|
|
||||||
const headers = {
|
|
||||||
...(options.headers || {}),
|
|
||||||
...(state.apiKey ? { 'X-API-Key': state.apiKey } : {}) // Use X-API-Key instead of Bearer
|
|
||||||
};
|
|
||||||
return fetch(url, { ...options, headers });
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Page renderers
|
|
||||||
const pages = {
|
|
||||||
'file-manager': () => `
|
|
||||||
<div class="space-y-6">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-800">File Manager</h2>
|
|
||||||
|
|
||||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors">
|
|
||||||
<input type="file" id="fileInput" multiple accept=".txt,.md,.doc,.docx,.pdf,.pptx" class="hidden">
|
|
||||||
<label for="fileInput" class="cursor-pointer">
|
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
|
||||||
</svg>
|
|
||||||
<p class="mt-2 text-gray-600">Drag files here or click to select</p>
|
|
||||||
<p class="text-sm text-gray-500">Supported formats: TXT, MD, DOC, PDF, PPTX</p>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="fileList" class="space-y-2">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-700">Selected Files</h3>
|
|
||||||
<div class="space-y-2"></div>
|
|
||||||
</div>
|
|
||||||
<div id="uploadProgress" class="hidden mt-4">
|
|
||||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
|
||||||
<div class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-600 mt-2"><span id="uploadStatus">0</span> files processed</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-4 bg-gray-100 p-4 rounded-lg shadow-md">
|
|
||||||
<button id="rescanBtn" class="flex items-center bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" class="mr-2">
|
|
||||||
<path d="M12 4a8 8 0 1 1-8 8H2.5a9.5 9.5 0 1 0 2.8-6.7L2 3v6h6L5.7 6.7A7.96 7.96 0 0 1 12 4z"/>
|
|
||||||
</svg>
|
|
||||||
Rescan Files
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="uploadBtn" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
|
||||||
Upload & Index Files
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="indexedFiles" class="space-y-2">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-700">Indexed Files</h3>
|
|
||||||
<div class="space-y-2"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
'query': () => `
|
|
||||||
<div class="space-y-6">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-800">Query Database</h2>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Query Mode</label>
|
|
||||||
<select id="queryMode" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
||||||
<option value="hybrid">Hybrid</option>
|
|
||||||
<option value="local">Local</option>
|
|
||||||
<option value="global">Global</option>
|
|
||||||
<option value="naive">Naive</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">Query</label>
|
|
||||||
<textarea id="queryInput" rows="4" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="queryBtn" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
|
||||||
Send Query
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="queryResult" class="mt-4 p-4 bg-white rounded-lg shadow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
'knowledge-graph': () => `
|
|
||||||
<div class="flex items-center justify-center h-full">
|
|
||||||
<div class="text-center">
|
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
||||||
</svg>
|
|
||||||
<h3 class="mt-2 text-sm font-medium text-gray-900">Under Construction</h3>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">Knowledge graph visualization will be available in a future update.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
'status': () => `
|
|
||||||
<div class="space-y-6">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-800">System Status</h2>
|
|
||||||
<div id="statusContent" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="p-6 bg-white rounded-lg shadow-sm">
|
|
||||||
<h3 class="text-lg font-semibold mb-4">System Health</h3>
|
|
||||||
<div id="healthStatus"></div>
|
|
||||||
</div>
|
|
||||||
<div class="p-6 bg-white rounded-lg shadow-sm">
|
|
||||||
<h3 class="text-lg font-semibold mb-4">Configuration</h3>
|
|
||||||
<div id="configStatus"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
|
|
||||||
'settings': () => `
|
|
||||||
<div class="space-y-6">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-800">Settings</h2>
|
|
||||||
|
|
||||||
<div class="max-w-xl">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700">API Key</label>
|
|
||||||
<input type="password" id="apiKeyInput" value="${state.apiKey}"
|
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="saveSettings" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors">
|
|
||||||
Save Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Page handlers
|
|
||||||
const handlers = {
|
|
||||||
'file-manager': () => {
|
|
||||||
const fileInput = document.getElementById('fileInput');
|
|
||||||
const dropZone = fileInput.parentElement.parentElement;
|
|
||||||
const fileList = document.querySelector('#fileList div');
|
|
||||||
const indexedFiles = document.querySelector('#indexedFiles div');
|
|
||||||
const uploadBtn = document.getElementById('uploadBtn');
|
|
||||||
|
|
||||||
const updateFileList = () => {
|
|
||||||
fileList.innerHTML = state.files.map(file => `
|
|
||||||
<div class="flex items-center justify-between bg-white p-3 rounded-lg shadow-sm">
|
|
||||||
<span>${file.name}</span>
|
|
||||||
<button class="text-red-600 hover:text-red-700" onclick="removeFile('${file.name}')">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateIndexedFiles = async () => {
|
|
||||||
const response = await fetchWithAuth('/health');
|
|
||||||
const data = await response.json();
|
|
||||||
indexedFiles.innerHTML = data.indexed_files.map(file => `
|
|
||||||
<div class="flex items-center justify-between bg-white p-3 rounded-lg shadow-sm">
|
|
||||||
<span>${file}</span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.add('border-blue-500');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', () => {
|
|
||||||
dropZone.classList.remove('border-blue-500');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('border-blue-500');
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
state.files.push(...files);
|
|
||||||
updateFileList();
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', () => {
|
|
||||||
state.files.push(...Array.from(fileInput.files));
|
|
||||||
updateFileList();
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadBtn.addEventListener('click', async () => {
|
|
||||||
if (state.files.length === 0) {
|
|
||||||
showToast('Please select files to upload');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let apiKey = localStorage.getItem('apiKey') || '';
|
|
||||||
const progress = document.getElementById('uploadProgress');
|
|
||||||
const progressBar = progress.querySelector('div');
|
|
||||||
const statusText = document.getElementById('uploadStatus');
|
|
||||||
progress.classList.remove('hidden');
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', state.files[i]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetch('/documents/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
const percentage = ((i + 1) / state.files.length) * 100;
|
|
||||||
progressBar.style.width = `${percentage}%`;
|
|
||||||
statusText.textContent = `${i + 1}/${state.files.length}`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload error:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
progress.classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
rescanBtn.addEventListener('click', async () => {
|
|
||||||
const progress = document.getElementById('uploadProgress');
|
|
||||||
const progressBar = progress.querySelector('div');
|
|
||||||
const statusText = document.getElementById('uploadStatus');
|
|
||||||
progress.classList.remove('hidden');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Start the scanning process
|
|
||||||
const scanResponse = await fetch('/documents/scan', {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!scanResponse.ok) {
|
|
||||||
throw new Error('Scan failed to start');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start polling for progress
|
|
||||||
const pollInterval = setInterval(async () => {
|
|
||||||
const progressResponse = await fetch('/documents/scan-progress');
|
|
||||||
const progressData = await progressResponse.json();
|
|
||||||
|
|
||||||
// Update progress bar
|
|
||||||
progressBar.style.width = `${progressData.progress}%`;
|
|
||||||
|
|
||||||
// Update status text
|
|
||||||
if (progressData.total_files > 0) {
|
|
||||||
statusText.textContent = `Processing ${progressData.current_file} (${progressData.indexed_count}/${progressData.total_files})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if scanning is complete
|
|
||||||
if (!progressData.is_scanning) {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
progress.classList.add('hidden');
|
|
||||||
statusText.textContent = 'Scan complete!';
|
|
||||||
}
|
|
||||||
}, 1000); // Poll every second
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload error:', error);
|
|
||||||
progress.classList.add('hidden');
|
|
||||||
statusText.textContent = 'Error during scanning process';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
updateIndexedFiles();
|
|
||||||
},
|
|
||||||
|
|
||||||
'query': () => {
|
|
||||||
const queryBtn = document.getElementById('queryBtn');
|
|
||||||
const queryInput = document.getElementById('queryInput');
|
|
||||||
const queryMode = document.getElementById('queryMode');
|
|
||||||
const queryResult = document.getElementById('queryResult');
|
|
||||||
|
|
||||||
let apiKey = localStorage.getItem('apiKey') || '';
|
|
||||||
|
|
||||||
queryBtn.addEventListener('click', async () => {
|
|
||||||
const query = queryInput.value.trim();
|
|
||||||
if (!query) {
|
|
||||||
showToast('Please enter a query');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queryBtn.disabled = true;
|
|
||||||
queryBtn.innerHTML = `
|
|
||||||
<svg class="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
|
||||||
</svg>
|
|
||||||
Processing...
|
|
||||||
`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth('/query', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
query,
|
|
||||||
mode: queryMode.value,
|
|
||||||
stream: false,
|
|
||||||
only_need_context: false
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
queryResult.innerHTML = marked.parse(data.response);
|
|
||||||
} catch (error) {
|
|
||||||
showToast('Error processing query');
|
|
||||||
} finally {
|
|
||||||
queryBtn.disabled = false;
|
|
||||||
queryBtn.textContent = 'Send Query';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'status': async () => {
|
|
||||||
const healthStatus = document.getElementById('healthStatus');
|
|
||||||
const configStatus = document.getElementById('configStatus');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchWithAuth('/health');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
healthStatus.innerHTML = `
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-3 h-3 rounded-full ${data.status === 'healthy' ? 'bg-green-500' : 'bg-red-500'} mr-2"></div>
|
|
||||||
<span class="font-medium">${data.status}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm text-gray-600">Working Directory: ${data.working_directory}</p>
|
|
||||||
<p class="text-sm text-gray-600">Input Directory: ${data.input_directory}</p>
|
|
||||||
<p class="text-sm text-gray-600">Indexed Files: ${data.indexed_files_count}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
configStatus.innerHTML = Object.entries(data.configuration)
|
|
||||||
.map(([key, value]) => `
|
|
||||||
<div class="mb-2">
|
|
||||||
<span class="text-sm font-medium text-gray-700">${key}:</span>
|
|
||||||
<span class="text-sm text-gray-600 ml-2">${value}</span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} catch (error) {
|
|
||||||
showToast('Error fetching status');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'settings': () => {
|
|
||||||
const saveBtn = document.getElementById('saveSettings');
|
|
||||||
const apiKeyInput = document.getElementById('apiKeyInput');
|
|
||||||
|
|
||||||
saveBtn.addEventListener('click', () => {
|
|
||||||
state.apiKey = apiKeyInput.value;
|
|
||||||
localStorage.setItem('apiKey', state.apiKey);
|
|
||||||
showToast('Settings saved successfully');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation handling
|
|
||||||
document.querySelectorAll('.nav-item').forEach(item => {
|
|
||||||
item.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const page = item.dataset.page;
|
|
||||||
document.getElementById('content').innerHTML = pages[page]();
|
|
||||||
if (handlers[page]) handlers[page]();
|
|
||||||
state.currentPage = page;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize with file manager
|
|
||||||
document.getElementById('content').innerHTML = pages['file-manager']();
|
|
||||||
handlers['file-manager']();
|
|
||||||
|
|
||||||
// Global functions
|
|
||||||
window.removeFile = (fileName) => {
|
|
||||||
state.files = state.files.filter(file => file.name !== fileName);
|
|
||||||
document.querySelector('#fileList div').innerHTML = state.files.map(file => `
|
|
||||||
<div class="flex items-center justify-between bg-white p-3 rounded-lg shadow-sm">
|
|
||||||
<span>${file.name}</span>
|
|
||||||
<button class="text-red-600 hover:text-red-700" onclick="removeFile('${file.name}')">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
};
|
|
@@ -1,211 +0,0 @@
|
|||||||
// js/graph.js
|
|
||||||
function openGraphModal(label) {
|
|
||||||
const modal = document.getElementById("graph-modal");
|
|
||||||
const graphTitle = document.getElementById("graph-title");
|
|
||||||
|
|
||||||
if (!modal || !graphTitle) {
|
|
||||||
console.error("Key element not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
graphTitle.textContent = `Knowledge Graph - ${label}`;
|
|
||||||
modal.style.display = "flex";
|
|
||||||
|
|
||||||
renderGraph(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeGraphModal() {
|
|
||||||
const modal = document.getElementById("graph-modal");
|
|
||||||
modal.style.display = "none";
|
|
||||||
clearGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearGraph() {
|
|
||||||
const svg = document.getElementById("graph-svg");
|
|
||||||
svg.innerHTML = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function getGraph(label) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/graphs?label=${label}`);
|
|
||||||
const rawData = await response.json();
|
|
||||||
console.log({data: JSON.parse(JSON.stringify(rawData))});
|
|
||||||
|
|
||||||
const nodes = rawData.nodes
|
|
||||||
|
|
||||||
nodes.forEach(node => {
|
|
||||||
node.id = Date.now().toString(36) + Math.random().toString(36).substring(2); // 使用 crypto.randomUUID() 生成唯一 UUID
|
|
||||||
});
|
|
||||||
|
|
||||||
// Strictly verify edge data
|
|
||||||
const edges = (rawData.edges || []).map(edge => {
|
|
||||||
const sourceNode = nodes.find(n => n.labels.includes(edge.source));
|
|
||||||
const targetNode = nodes.find(n => n.labels.includes(edge.target)
|
|
||||||
)
|
|
||||||
;
|
|
||||||
if (!sourceNode || !targetNode) {
|
|
||||||
console.warn("NOT VALID EDGE:", edge);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
source: sourceNode,
|
|
||||||
target: targetNode,
|
|
||||||
type: edge.type || ""
|
|
||||||
};
|
|
||||||
}).filter(edge => edge !== null);
|
|
||||||
|
|
||||||
return {nodes, edges};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Loading graph failed:", error);
|
|
||||||
return {nodes: [], edges: []};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderGraph(label) {
|
|
||||||
const data = await getGraph(label);
|
|
||||||
|
|
||||||
|
|
||||||
if (!data.nodes || data.nodes.length === 0) {
|
|
||||||
d3.select("#graph-svg")
|
|
||||||
.html(`<text x="50%" y="50%" text-anchor="middle">No valid nodes</text>`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const svg = d3.select("#graph-svg");
|
|
||||||
const width = svg.node().clientWidth;
|
|
||||||
const height = svg.node().clientHeight;
|
|
||||||
|
|
||||||
svg.selectAll("*").remove();
|
|
||||||
|
|
||||||
// Create a force oriented diagram layout
|
|
||||||
const simulation = d3.forceSimulation(data.nodes)
|
|
||||||
.force("charge", d3.forceManyBody().strength(-300))
|
|
||||||
.force("center", d3.forceCenter(width / 2, height / 2));
|
|
||||||
|
|
||||||
// Add a connection (if there are valid edges)
|
|
||||||
if (data.edges.length > 0) {
|
|
||||||
simulation.force("link",
|
|
||||||
d3.forceLink(data.edges)
|
|
||||||
.id(d => d.id)
|
|
||||||
.distance(100)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw nodes
|
|
||||||
const nodes = svg.selectAll(".node")
|
|
||||||
.data(data.nodes)
|
|
||||||
.enter()
|
|
||||||
.append("circle")
|
|
||||||
.attr("class", "node")
|
|
||||||
.attr("r", 10)
|
|
||||||
.call(d3.drag()
|
|
||||||
.on("start", dragStarted)
|
|
||||||
.on("drag", dragged)
|
|
||||||
.on("end", dragEnded)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
svg.append("defs")
|
|
||||||
.append("marker")
|
|
||||||
.attr("id", "arrow-out")
|
|
||||||
.attr("viewBox", "0 0 10 10")
|
|
||||||
.attr("refX", 8)
|
|
||||||
.attr("refY", 5)
|
|
||||||
.attr("markerWidth", 6)
|
|
||||||
.attr("markerHeight", 6)
|
|
||||||
.attr("orient", "auto")
|
|
||||||
.append("path")
|
|
||||||
.attr("d", "M0,0 L10,5 L0,10 Z")
|
|
||||||
.attr("fill", "#999");
|
|
||||||
|
|
||||||
// Draw edges (with arrows)
|
|
||||||
const links = svg.selectAll(".link")
|
|
||||||
.data(data.edges)
|
|
||||||
.enter()
|
|
||||||
.append("line")
|
|
||||||
.attr("class", "link")
|
|
||||||
.attr("marker-end", "url(#arrow-out)"); // Always draw arrows on the target side
|
|
||||||
|
|
||||||
// Edge style configuration
|
|
||||||
links
|
|
||||||
.attr("stroke", "#999")
|
|
||||||
.attr("stroke-width", 2)
|
|
||||||
.attr("stroke-opacity", 0.8);
|
|
||||||
|
|
||||||
// Draw label (with background box)
|
|
||||||
const labels = svg.selectAll(".label")
|
|
||||||
.data(data.nodes)
|
|
||||||
.enter()
|
|
||||||
.append("text")
|
|
||||||
.attr("class", "label")
|
|
||||||
.text(d => d.labels[0] || "")
|
|
||||||
.attr("text-anchor", "start")
|
|
||||||
.attr("dy", "0.3em")
|
|
||||||
.attr("fill", "#333");
|
|
||||||
|
|
||||||
// Update Location
|
|
||||||
simulation.on("tick", () => {
|
|
||||||
links
|
|
||||||
.attr("x1", d => {
|
|
||||||
// Calculate the direction vector from the source node to the target node
|
|
||||||
const dx = d.target.x - d.source.x;
|
|
||||||
const dy = d.target.y - d.source.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance === 0) return d.source.x; // 避免除以零 Avoid dividing by zero
|
|
||||||
// Adjust the starting point coordinates (source node edge) based on radius 10
|
|
||||||
return d.source.x + (dx / distance) * 10;
|
|
||||||
})
|
|
||||||
.attr("y1", d => {
|
|
||||||
const dx = d.target.x - d.source.x;
|
|
||||||
const dy = d.target.y - d.source.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance === 0) return d.source.y;
|
|
||||||
return d.source.y + (dy / distance) * 10;
|
|
||||||
})
|
|
||||||
.attr("x2", d => {
|
|
||||||
// Adjust the endpoint coordinates (target node edge) based on a radius of 10
|
|
||||||
const dx = d.target.x - d.source.x;
|
|
||||||
const dy = d.target.y - d.source.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance === 0) return d.target.x;
|
|
||||||
return d.target.x - (dx / distance) * 10;
|
|
||||||
})
|
|
||||||
.attr("y2", d => {
|
|
||||||
const dx = d.target.x - d.source.x;
|
|
||||||
const dy = d.target.y - d.source.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
if (distance === 0) return d.target.y;
|
|
||||||
return d.target.y - (dy / distance) * 10;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the position of nodes and labels (keep unchanged)
|
|
||||||
nodes
|
|
||||||
.attr("cx", d => d.x)
|
|
||||||
.attr("cy", d => d.y);
|
|
||||||
|
|
||||||
labels
|
|
||||||
.attr("x", d => d.x + 12)
|
|
||||||
.attr("y", d => d.y + 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drag and drop logic
|
|
||||||
function dragStarted(event, d) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
||||||
d.fx = d.x;
|
|
||||||
d.fy = d.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragged(event, d) {
|
|
||||||
d.fx = event.x;
|
|
||||||
d.fy = event.y;
|
|
||||||
simulation.alpha(0.3).restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragEnded(event, d) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
|
||||||
d.fx = null;
|
|
||||||
d.fy = null;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
1065
lightrag/api/webui/assets/index-BMB0OroL.js
Normal file
1065
lightrag/api/webui/assets/index-BMB0OroL.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-CLgSwrjG.css
Normal file
1
lightrag/api/webui/assets/index-CLgSwrjG.css
Normal file
File diff suppressed because one or more lines are too long
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag Graph Viewer</title>
|
<title>Lightrag</title>
|
||||||
<script type="module" crossorigin src="./assets/index-CF-pcoIl.js"></script>
|
<script type="module" crossorigin src="./assets/index-BMB0OroL.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BAeLPZpd.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-CLgSwrjG.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import (
|
from typing import (
|
||||||
@@ -9,12 +10,12 @@ from typing import (
|
|||||||
TypedDict,
|
TypedDict,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
)
|
)
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .utils import EmbeddingFunc
|
from .utils import EmbeddingFunc
|
||||||
from .types import KnowledgeGraph
|
from .types import KnowledgeGraph
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
class TextChunkSchema(TypedDict):
|
class TextChunkSchema(TypedDict):
|
||||||
tokens: int
|
tokens: int
|
||||||
@@ -54,13 +55,15 @@ class QueryParam:
|
|||||||
top_k: int = int(os.getenv("TOP_K", "60"))
|
top_k: int = int(os.getenv("TOP_K", "60"))
|
||||||
"""Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode."""
|
"""Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode."""
|
||||||
|
|
||||||
max_token_for_text_unit: int = 4000
|
max_token_for_text_unit: int = int(os.getenv("MAX_TOKEN_TEXT_CHUNK", "4000"))
|
||||||
"""Maximum number of tokens allowed for each retrieved text chunk."""
|
"""Maximum number of tokens allowed for each retrieved text chunk."""
|
||||||
|
|
||||||
max_token_for_global_context: int = 4000
|
max_token_for_global_context: int = int(
|
||||||
|
os.getenv("MAX_TOKEN_RELATION_DESC", "4000")
|
||||||
|
)
|
||||||
"""Maximum number of tokens allocated for relationship descriptions in global retrieval."""
|
"""Maximum number of tokens allocated for relationship descriptions in global retrieval."""
|
||||||
|
|
||||||
max_token_for_local_context: int = 4000
|
max_token_for_local_context: int = int(os.getenv("MAX_TOKEN_ENTITY_DESC", "4000"))
|
||||||
"""Maximum number of tokens allocated for entity descriptions in local retrieval."""
|
"""Maximum number of tokens allocated for entity descriptions in local retrieval."""
|
||||||
|
|
||||||
hl_keywords: list[str] = field(default_factory=list)
|
hl_keywords: list[str] = field(default_factory=list)
|
||||||
|
@@ -268,10 +268,10 @@ class LightRAG:
|
|||||||
"""Directory where logs are stored. Defaults to the current working directory."""
|
"""Directory where logs are stored. Defaults to the current working directory."""
|
||||||
|
|
||||||
# Text chunking
|
# Text chunking
|
||||||
chunk_token_size: int = 1200
|
chunk_token_size: int = int(os.getenv("CHUNK_SIZE", "1200"))
|
||||||
"""Maximum number of tokens per text chunk when splitting documents."""
|
"""Maximum number of tokens per text chunk when splitting documents."""
|
||||||
|
|
||||||
chunk_overlap_token_size: int = 100
|
chunk_overlap_token_size: int = int(os.getenv("CHUNK_OVERLAP_SIZE", "100"))
|
||||||
"""Number of overlapping tokens between consecutive text chunks to preserve context."""
|
"""Number of overlapping tokens between consecutive text chunks to preserve context."""
|
||||||
|
|
||||||
tiktoken_model_name: str = "gpt-4o-mini"
|
tiktoken_model_name: str = "gpt-4o-mini"
|
||||||
@@ -281,7 +281,7 @@ class LightRAG:
|
|||||||
entity_extract_max_gleaning: int = 1
|
entity_extract_max_gleaning: int = 1
|
||||||
"""Maximum number of entity extraction attempts for ambiguous content."""
|
"""Maximum number of entity extraction attempts for ambiguous content."""
|
||||||
|
|
||||||
entity_summary_to_max_tokens: int = 500
|
entity_summary_to_max_tokens: int = int(os.getenv("MAX_TOKEN_SUMMARY", "500"))
|
||||||
"""Maximum number of tokens used for summarizing extracted entities."""
|
"""Maximum number of tokens used for summarizing extracted entities."""
|
||||||
|
|
||||||
# Node embedding
|
# Node embedding
|
||||||
@@ -1254,6 +1254,16 @@ class LightRAG:
|
|||||||
"""
|
"""
|
||||||
return await self.doc_status.get_status_counts()
|
return await self.doc_status.get_status_counts()
|
||||||
|
|
||||||
|
async def get_docs_by_status(
|
||||||
|
self, status: DocStatus
|
||||||
|
) -> dict[str, DocProcessingStatus]:
|
||||||
|
"""Get documents by status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with document id is keys and document status is values
|
||||||
|
"""
|
||||||
|
return await self.doc_status.get_docs_by_status(status)
|
||||||
|
|
||||||
async def adelete_by_doc_id(self, doc_id: str) -> None:
|
async def adelete_by_doc_id(self, doc_id: str) -> None:
|
||||||
"""Delete a document and all its related data
|
"""Delete a document and all its related data
|
||||||
|
|
||||||
|
@@ -40,9 +40,10 @@ __version__ = "1.0.0"
|
|||||||
__author__ = "lightrag Team"
|
__author__ = "lightrag Team"
|
||||||
__status__ = "Production"
|
__status__ = "Production"
|
||||||
|
|
||||||
|
from ..utils import verbose_debug, VERBOSE_DEBUG
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
if sys.version_info < (3, 9):
|
if sys.version_info < (3, 9):
|
||||||
from typing import AsyncIterator
|
from typing import AsyncIterator
|
||||||
@@ -110,6 +111,11 @@ async def openai_complete_if_cache(
|
|||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_8) LightRAG/{__api_version__}",
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_8) LightRAG/{__api_version__}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Set openai logger level to INFO when VERBOSE_DEBUG is off
|
||||||
|
if not VERBOSE_DEBUG and logger.level == logging.DEBUG:
|
||||||
|
logging.getLogger("openai").setLevel(logging.INFO)
|
||||||
|
|
||||||
openai_async_client = (
|
openai_async_client = (
|
||||||
AsyncOpenAI(default_headers=default_headers, api_key=api_key)
|
AsyncOpenAI(default_headers=default_headers, api_key=api_key)
|
||||||
if base_url is None
|
if base_url is None
|
||||||
@@ -125,13 +131,11 @@ async def openai_complete_if_cache(
|
|||||||
messages.extend(history_messages)
|
messages.extend(history_messages)
|
||||||
messages.append({"role": "user", "content": prompt})
|
messages.append({"role": "user", "content": prompt})
|
||||||
|
|
||||||
# 添加日志输出
|
logger.debug("===== Sending Query to LLM =====")
|
||||||
logger.debug("===== Query Input to LLM =====")
|
|
||||||
logger.debug(f"Model: {model} Base URL: {base_url}")
|
logger.debug(f"Model: {model} Base URL: {base_url}")
|
||||||
logger.debug(f"Additional kwargs: {kwargs}")
|
logger.debug(f"Additional kwargs: {kwargs}")
|
||||||
logger.debug(f"Query: {prompt}")
|
verbose_debug(f"Query: {prompt}")
|
||||||
logger.debug(f"System prompt: {system_prompt}")
|
verbose_debug(f"System prompt: {system_prompt}")
|
||||||
# logger.debug(f"Messages: {messages}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if "response_format" in kwargs:
|
if "response_format" in kwargs:
|
||||||
|
@@ -43,6 +43,7 @@ __status__ = "Production"
|
|||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
from ..utils import verbose_debug
|
||||||
|
|
||||||
if sys.version_info < (3, 9):
|
if sys.version_info < (3, 9):
|
||||||
pass
|
pass
|
||||||
@@ -119,7 +120,7 @@ async def zhipu_complete_if_cache(
|
|||||||
# Add debug logging
|
# Add debug logging
|
||||||
logger.debug("===== Query Input to LLM =====")
|
logger.debug("===== Query Input to LLM =====")
|
||||||
logger.debug(f"Query: {prompt}")
|
logger.debug(f"Query: {prompt}")
|
||||||
logger.debug(f"System prompt: {system_prompt}")
|
verbose_debug(f"System prompt: {system_prompt}")
|
||||||
|
|
||||||
# Remove unsupported kwargs
|
# Remove unsupported kwargs
|
||||||
kwargs = {
|
kwargs = {
|
||||||
|
@@ -687,6 +687,9 @@ async def kg_query(
|
|||||||
if query_param.only_need_prompt:
|
if query_param.only_need_prompt:
|
||||||
return sys_prompt
|
return sys_prompt
|
||||||
|
|
||||||
|
len_of_prompts = len(encode_string_by_tiktoken(query + sys_prompt))
|
||||||
|
logger.debug(f"[kg_query]Prompt Tokens: {len_of_prompts}")
|
||||||
|
|
||||||
response = await use_model_func(
|
response = await use_model_func(
|
||||||
query,
|
query,
|
||||||
system_prompt=sys_prompt,
|
system_prompt=sys_prompt,
|
||||||
@@ -772,6 +775,9 @@ async def extract_keywords_only(
|
|||||||
query=text, examples=examples, language=language, history=history_context
|
query=text, examples=examples, language=language, history=history_context
|
||||||
)
|
)
|
||||||
|
|
||||||
|
len_of_prompts = len(encode_string_by_tiktoken(kw_prompt))
|
||||||
|
logger.debug(f"[kg_query]Prompt Tokens: {len_of_prompts}")
|
||||||
|
|
||||||
# 5. Call the LLM for keyword extraction
|
# 5. Call the LLM for keyword extraction
|
||||||
use_model_func = global_config["llm_model_func"]
|
use_model_func = global_config["llm_model_func"]
|
||||||
result = await use_model_func(kw_prompt, keyword_extraction=True)
|
result = await use_model_func(kw_prompt, keyword_extraction=True)
|
||||||
@@ -935,7 +941,9 @@ async def mix_kg_vector_query(
|
|||||||
chunk_text = f"[Created at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(c['created_at']))}]\n{chunk_text}"
|
chunk_text = f"[Created at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(c['created_at']))}]\n{chunk_text}"
|
||||||
formatted_chunks.append(chunk_text)
|
formatted_chunks.append(chunk_text)
|
||||||
|
|
||||||
logger.info(f"Truncate {len(chunks)} to {len(formatted_chunks)} chunks")
|
logger.debug(
|
||||||
|
f"Truncate chunks from {len(chunks)} to {len(formatted_chunks)} (max tokens:{query_param.max_token_for_text_unit})"
|
||||||
|
)
|
||||||
return "\n--New Chunk--\n".join(formatted_chunks)
|
return "\n--New Chunk--\n".join(formatted_chunks)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in get_vector_context: {e}")
|
logger.error(f"Error in get_vector_context: {e}")
|
||||||
@@ -968,6 +976,9 @@ async def mix_kg_vector_query(
|
|||||||
if query_param.only_need_prompt:
|
if query_param.only_need_prompt:
|
||||||
return sys_prompt
|
return sys_prompt
|
||||||
|
|
||||||
|
len_of_prompts = len(encode_string_by_tiktoken(query + sys_prompt))
|
||||||
|
logger.debug(f"[mix_kg_vector_query]Prompt Tokens: {len_of_prompts}")
|
||||||
|
|
||||||
# 6. Generate response
|
# 6. Generate response
|
||||||
response = await use_model_func(
|
response = await use_model_func(
|
||||||
query,
|
query,
|
||||||
@@ -1073,7 +1084,7 @@ async def _build_query_context(
|
|||||||
if not entities_context.strip() and not relations_context.strip():
|
if not entities_context.strip() and not relations_context.strip():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return f"""
|
result = f"""
|
||||||
-----Entities-----
|
-----Entities-----
|
||||||
```csv
|
```csv
|
||||||
{entities_context}
|
{entities_context}
|
||||||
@@ -1087,6 +1098,15 @@ async def _build_query_context(
|
|||||||
{text_units_context}
|
{text_units_context}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
contex_tokens = len(encode_string_by_tiktoken(result))
|
||||||
|
entities_tokens = len(encode_string_by_tiktoken(entities_context))
|
||||||
|
relations_tokens = len(encode_string_by_tiktoken(relations_context))
|
||||||
|
text_units_tokens = len(encode_string_by_tiktoken(text_units_context))
|
||||||
|
logger.debug(
|
||||||
|
f"Context Tokens - Total: {contex_tokens}, Entities: {entities_tokens}, Relations: {relations_tokens}, Chunks: {text_units_tokens}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def _get_node_data(
|
async def _get_node_data(
|
||||||
@@ -1130,8 +1150,19 @@ async def _get_node_data(
|
|||||||
node_datas, query_param, knowledge_graph_inst
|
node_datas, query_param, knowledge_graph_inst
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
len_node_datas = len(node_datas)
|
||||||
|
node_datas = truncate_list_by_token_size(
|
||||||
|
node_datas,
|
||||||
|
key=lambda x: x["description"],
|
||||||
|
max_token_size=query_param.max_token_for_local_context,
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"Truncate entities from {len_node_datas} to {len(node_datas)} (max tokens:{query_param.max_token_for_local_context})"
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Local query uses {len(node_datas)} entites, {len(use_relations)} relations, {len(use_text_units)} text units"
|
f"Local query uses {len(node_datas)} entites, {len(use_relations)} relations, {len(use_text_units)} chunks"
|
||||||
)
|
)
|
||||||
|
|
||||||
# build prompt
|
# build prompt
|
||||||
@@ -1264,6 +1295,10 @@ async def _find_most_related_text_unit_from_entities(
|
|||||||
max_token_size=query_param.max_token_for_text_unit,
|
max_token_size=query_param.max_token_for_text_unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Truncate chunks from {len(all_text_units_lookup)} to {len(all_text_units)} (max tokens:{query_param.max_token_for_text_unit})"
|
||||||
|
)
|
||||||
|
|
||||||
all_text_units = [t["data"] for t in all_text_units]
|
all_text_units = [t["data"] for t in all_text_units]
|
||||||
return all_text_units
|
return all_text_units
|
||||||
|
|
||||||
@@ -1305,6 +1340,11 @@ async def _find_most_related_edges_from_entities(
|
|||||||
key=lambda x: x["description"],
|
key=lambda x: x["description"],
|
||||||
max_token_size=query_param.max_token_for_global_context,
|
max_token_size=query_param.max_token_for_global_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Truncate relations from {len(all_edges)} to {len(all_edges_data)} (max tokens:{query_param.max_token_for_global_context})"
|
||||||
|
)
|
||||||
|
|
||||||
return all_edges_data
|
return all_edges_data
|
||||||
|
|
||||||
|
|
||||||
@@ -1352,11 +1392,15 @@ async def _get_edge_data(
|
|||||||
edge_datas = sorted(
|
edge_datas = sorted(
|
||||||
edge_datas, key=lambda x: (x["rank"], x["weight"]), reverse=True
|
edge_datas, key=lambda x: (x["rank"], x["weight"]), reverse=True
|
||||||
)
|
)
|
||||||
|
len_edge_datas = len(edge_datas)
|
||||||
edge_datas = truncate_list_by_token_size(
|
edge_datas = truncate_list_by_token_size(
|
||||||
edge_datas,
|
edge_datas,
|
||||||
key=lambda x: x["description"],
|
key=lambda x: x["description"],
|
||||||
max_token_size=query_param.max_token_for_global_context,
|
max_token_size=query_param.max_token_for_global_context,
|
||||||
)
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"Truncate relations from {len_edge_datas} to {len(edge_datas)} (max tokens:{query_param.max_token_for_global_context})"
|
||||||
|
)
|
||||||
|
|
||||||
use_entities, use_text_units = await asyncio.gather(
|
use_entities, use_text_units = await asyncio.gather(
|
||||||
_find_most_related_entities_from_relationships(
|
_find_most_related_entities_from_relationships(
|
||||||
@@ -1367,7 +1411,7 @@ async def _get_edge_data(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Global query uses {len(use_entities)} entites, {len(edge_datas)} relations, {len(use_text_units)} text units"
|
f"Global query uses {len(use_entities)} entites, {len(edge_datas)} relations, {len(use_text_units)} chunks"
|
||||||
)
|
)
|
||||||
|
|
||||||
relations_section_list = [
|
relations_section_list = [
|
||||||
@@ -1456,11 +1500,15 @@ async def _find_most_related_entities_from_relationships(
|
|||||||
for k, n, d in zip(entity_names, node_datas, node_degrees)
|
for k, n, d in zip(entity_names, node_datas, node_degrees)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
len_node_datas = len(node_datas)
|
||||||
node_datas = truncate_list_by_token_size(
|
node_datas = truncate_list_by_token_size(
|
||||||
node_datas,
|
node_datas,
|
||||||
key=lambda x: x["description"],
|
key=lambda x: x["description"],
|
||||||
max_token_size=query_param.max_token_for_local_context,
|
max_token_size=query_param.max_token_for_local_context,
|
||||||
)
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"Truncate entities from {len_node_datas} to {len(node_datas)} (max tokens:{query_param.max_token_for_local_context})"
|
||||||
|
)
|
||||||
|
|
||||||
return node_datas
|
return node_datas
|
||||||
|
|
||||||
@@ -1516,6 +1564,10 @@ async def _find_related_text_unit_from_relationships(
|
|||||||
max_token_size=query_param.max_token_for_text_unit,
|
max_token_size=query_param.max_token_for_text_unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Truncate chunks from {len(valid_text_units)} to {len(truncated_text_units)} (max tokens:{query_param.max_token_for_text_unit})"
|
||||||
|
)
|
||||||
|
|
||||||
all_text_units: list[TextChunkSchema] = [t["data"] for t in truncated_text_units]
|
all_text_units: list[TextChunkSchema] = [t["data"] for t in truncated_text_units]
|
||||||
|
|
||||||
return all_text_units
|
return all_text_units
|
||||||
@@ -1583,7 +1635,10 @@ async def naive_query(
|
|||||||
logger.warning("No chunks left after truncation")
|
logger.warning("No chunks left after truncation")
|
||||||
return PROMPTS["fail_response"]
|
return PROMPTS["fail_response"]
|
||||||
|
|
||||||
logger.info(f"Truncate {len(chunks)} to {len(maybe_trun_chunks)} chunks")
|
logger.debug(
|
||||||
|
f"Truncate chunks from {len(chunks)} to {len(maybe_trun_chunks)} (max tokens:{query_param.max_token_for_text_unit})"
|
||||||
|
)
|
||||||
|
|
||||||
section = "\n--New Chunk--\n".join([c["content"] for c in maybe_trun_chunks])
|
section = "\n--New Chunk--\n".join([c["content"] for c in maybe_trun_chunks])
|
||||||
|
|
||||||
if query_param.only_need_context:
|
if query_param.only_need_context:
|
||||||
@@ -1606,6 +1661,9 @@ async def naive_query(
|
|||||||
if query_param.only_need_prompt:
|
if query_param.only_need_prompt:
|
||||||
return sys_prompt
|
return sys_prompt
|
||||||
|
|
||||||
|
len_of_prompts = len(encode_string_by_tiktoken(query + sys_prompt))
|
||||||
|
logger.debug(f"[naive_query]Prompt Tokens: {len_of_prompts}")
|
||||||
|
|
||||||
response = await use_model_func(
|
response = await use_model_func(
|
||||||
query,
|
query,
|
||||||
system_prompt=sys_prompt,
|
system_prompt=sys_prompt,
|
||||||
@@ -1748,6 +1806,9 @@ async def kg_query_with_keywords(
|
|||||||
if query_param.only_need_prompt:
|
if query_param.only_need_prompt:
|
||||||
return sys_prompt
|
return sys_prompt
|
||||||
|
|
||||||
|
len_of_prompts = len(encode_string_by_tiktoken(query + sys_prompt))
|
||||||
|
logger.debug(f"[kg_query_with_keywords]Prompt Tokens: {len_of_prompts}")
|
||||||
|
|
||||||
response = await use_model_func(
|
response = await use_model_func(
|
||||||
query,
|
query,
|
||||||
system_prompt=sys_prompt,
|
system_prompt=sys_prompt,
|
||||||
|
@@ -20,6 +20,23 @@ import tiktoken
|
|||||||
|
|
||||||
from lightrag.prompt import PROMPTS
|
from lightrag.prompt import PROMPTS
|
||||||
|
|
||||||
|
VERBOSE_DEBUG = os.getenv("VERBOSE", "false").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def verbose_debug(msg: str, *args, **kwargs):
|
||||||
|
"""Function for outputting detailed debug information.
|
||||||
|
When VERBOSE_DEBUG=True, outputs the complete message.
|
||||||
|
When VERBOSE_DEBUG=False, outputs only the first 30 characters.
|
||||||
|
"""
|
||||||
|
if VERBOSE_DEBUG:
|
||||||
|
logger.debug(msg, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def set_verbose_debug(enabled: bool):
|
||||||
|
"""Enable or disable verbose debug output"""
|
||||||
|
global VERBOSE_DEBUG
|
||||||
|
VERBOSE_DEBUG = enabled
|
||||||
|
|
||||||
|
|
||||||
class UnlimitedSemaphore:
|
class UnlimitedSemaphore:
|
||||||
"""A context manager that allows unlimited access."""
|
"""A context manager that allows unlimited access."""
|
||||||
@@ -657,6 +674,10 @@ def get_conversation_turns(
|
|||||||
Returns:
|
Returns:
|
||||||
Formatted string of the conversation history
|
Formatted string of the conversation history
|
||||||
"""
|
"""
|
||||||
|
# Check if num_turns is valid
|
||||||
|
if num_turns <= 0:
|
||||||
|
return ""
|
||||||
|
|
||||||
# Group messages into turns
|
# Group messages into turns
|
||||||
turns: list[list[dict[str, Any]]] = []
|
turns: list[list[dict[str, Any]]] = []
|
||||||
messages: list[dict[str, Any]] = []
|
messages: list[dict[str, Any]] = []
|
||||||
|
@@ -21,7 +21,7 @@ LightRAG WebUI is a React-based web interface for interacting with the LightRAG
|
|||||||
Run the following command to build the project:
|
Run the following command to build the project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run build
|
bun run build --emptyOutDir
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will bundle the project and output the built files to the `lightrag/api/webui` directory.
|
This command will bundle the project and output the built files to the `lightrag/api/webui` directory.
|
||||||
|
@@ -4,13 +4,19 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lightrag-webui",
|
"name": "lightrag-webui",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^9.4.0",
|
"@faker-js/faker": "^9.5.0",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-popover": "^1.1.6",
|
"@radix-ui/react-popover": "^1.1.6",
|
||||||
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.1.0",
|
||||||
"@react-sigma/core": "^5.0.2",
|
"@react-sigma/core": "^5.0.2",
|
||||||
"@react-sigma/graph-search": "^5.0.3",
|
"@react-sigma/graph-search": "^5.0.3",
|
||||||
"@react-sigma/layout-circlepack": "^5.0.2",
|
"@react-sigma/layout-circlepack": "^5.0.2",
|
||||||
@@ -22,6 +28,7 @@
|
|||||||
"@react-sigma/minimap": "^5.0.2",
|
"@react-sigma/minimap": "^5.0.2",
|
||||||
"@sigma/edge-curve": "^3.1.0",
|
"@sigma/edge-curve": "^3.1.0",
|
||||||
"@sigma/node-border": "^3.0.0",
|
"@sigma/node-border": "^3.0.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
@@ -31,9 +38,13 @@
|
|||||||
"minisearch": "^7.1.1",
|
"minisearch": "^7.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-dropzone": "^14.3.5",
|
||||||
|
"react-number-format": "^5.4.3",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
"sigma": "^3.0.1",
|
"sigma": "^3.0.1",
|
||||||
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
|
"tailwind-scrollbar": "^4.0.0",
|
||||||
"zustand": "^5.0.3",
|
"zustand": "^5.0.3",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -41,19 +52,19 @@
|
|||||||
"@stylistic/eslint-plugin-js": "^3.1.0",
|
"@stylistic/eslint-plugin-js": "^3.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.13.4",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
"eslint": "^9.20.0",
|
"eslint": "^9.20.1",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.15.0",
|
||||||
"graphology-types": "^0.24.8",
|
"graphology-types": "^0.24.8",
|
||||||
"prettier": "^3.5.0",
|
"prettier": "^3.5.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -172,7 +183,7 @@
|
|||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
|
||||||
|
|
||||||
"@faker-js/faker": ["@faker-js/faker@9.4.0", "", {}, "sha512-85+k0AxaZSTowL0gXp8zYWDIrWclTbRPg/pm/V0dSFZ6W6D4lhcG3uuZl4zLsEKfEvs69xDbLN2cHQudwp95JA=="],
|
"@faker-js/faker": ["@faker-js/faker@9.5.0", "", {}, "sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw=="],
|
||||||
|
|
||||||
"@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
|
"@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="],
|
||||||
|
|
||||||
@@ -206,18 +217,26 @@
|
|||||||
|
|
||||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||||
|
|
||||||
|
"@radix-ui/number": ["@radix-ui/number@1.1.0", "", {}, "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dialog": "1.1.6", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
|
||||||
|
|
||||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="],
|
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="],
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||||
|
|
||||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
|
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="],
|
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="],
|
||||||
|
|
||||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
|
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
|
||||||
|
|
||||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
|
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="],
|
||||||
@@ -236,10 +255,20 @@
|
|||||||
|
|
||||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.2", "", { "dependencies": { "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.3", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-select": ["@radix-ui/react-select@2.1.6", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg=="],
|
||||||
|
|
||||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="],
|
||||||
@@ -384,10 +413,12 @@
|
|||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
|
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
|
||||||
|
|
||||||
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
|
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
|
||||||
|
|
||||||
|
"@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.0.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="],
|
"@types/react": ["@types/react@19.0.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.0.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="],
|
"@types/react-dom": ["@types/react-dom@19.0.3", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA=="],
|
||||||
@@ -446,8 +477,14 @@
|
|||||||
|
|
||||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
|
||||||
|
|
||||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="],
|
||||||
|
|
||||||
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
|
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
@@ -478,6 +515,8 @@
|
|||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
"convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
"convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
|
||||||
@@ -502,6 +541,8 @@
|
|||||||
|
|
||||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
@@ -536,7 +577,7 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@9.20.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA=="],
|
"eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="],
|
||||||
|
|
||||||
"eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="],
|
"eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="],
|
||||||
|
|
||||||
@@ -574,6 +615,8 @@
|
|||||||
|
|
||||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
|
|
||||||
|
"file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
|
||||||
|
|
||||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
|
||||||
@@ -584,8 +627,12 @@
|
|||||||
|
|
||||||
"flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
|
"flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||||
|
|
||||||
"for-each": ["for-each@0.3.4", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw=="],
|
"for-each": ["for-each@0.3.4", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
@@ -604,7 +651,7 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
"globals": ["globals@15.14.0", "", {}, "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig=="],
|
"globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||||
|
|
||||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
|
|
||||||
@@ -778,6 +825,10 @@
|
|||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
"minisearch": ["minisearch@7.1.1", "", {}, "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw=="],
|
"minisearch": ["minisearch@7.1.1", "", {}, "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw=="],
|
||||||
@@ -838,12 +889,16 @@
|
|||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.5.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA=="],
|
"prettier": ["prettier@3.5.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw=="],
|
||||||
|
|
||||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
|
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.11", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA=="],
|
||||||
|
|
||||||
|
"prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
|
||||||
|
|
||||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
@@ -852,8 +907,12 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
|
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
|
||||||
|
|
||||||
|
"react-dropzone": ["react-dropzone@14.3.5", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ=="],
|
||||||
|
|
||||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"react-number-format": ["react-number-format@5.4.3", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VCY5hFg/soBighAoGcdE+GagkJq0230qN6jcS5sp8wQX1qy1fYN/RX7/BXkrs0oyzzwqR8/+eSUrqXbGeywdUQ=="],
|
||||||
|
|
||||||
"react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
|
"react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
|
||||||
|
|
||||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
@@ -912,6 +971,8 @@
|
|||||||
|
|
||||||
"sigma": ["sigma@3.0.1", "", { "dependencies": { "events": "^3.3.0", "graphology-utils": "^2.5.2" } }, "sha512-z67BX1FhIpD+wLs2WJ7QS2aR49TcSr3YaVZ2zU8cAc5jMiUYlSbeDp4EI6euBDUpm3/lzO4pfytP/gW4BhXWuA=="],
|
"sigma": ["sigma@3.0.1", "", { "dependencies": { "events": "^3.3.0", "graphology-utils": "^2.5.2" } }, "sha512-z67BX1FhIpD+wLs2WJ7QS2aR49TcSr3YaVZ2zU8cAc5jMiUYlSbeDp4EI6euBDUpm3/lzO4pfytP/gW4BhXWuA=="],
|
||||||
|
|
||||||
|
"sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
@@ -936,6 +997,8 @@
|
|||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
|
"tailwind-merge": ["tailwind-merge@3.0.1", "", {}, "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g=="],
|
||||||
|
|
||||||
|
"tailwind-scrollbar": ["tailwind-scrollbar@4.0.0", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-elqx9m09VHY8gkrMiyimFO09JlS3AyLFXT0eaLaWPi7ImwHlbZj1ce/AxSis2LtR+ewBGEyUV7URNEMcjP1Z2w=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.0.6", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="],
|
"tailwindcss": ["tailwindcss@4.0.6", "", {}, "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw=="],
|
||||||
|
|
||||||
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
||||||
@@ -1006,12 +1069,16 @@
|
|||||||
|
|
||||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||||
|
|
||||||
|
"@types/ws/@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
|
||||||
|
|
||||||
"babel-plugin-macros/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
"babel-plugin-macros/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||||
|
|
||||||
|
"bun-types/@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
@@ -7,8 +7,10 @@ import tseslint from 'typescript-eslint'
|
|||||||
import prettier from 'eslint-config-prettier'
|
import prettier from 'eslint-config-prettier'
|
||||||
import react from 'eslint-plugin-react'
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
export default tseslint.config({ ignores: ['dist'] }, prettier, {
|
export default tseslint.config(
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
|
||||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
@@ -30,4 +32,5 @@ export default tseslint.config({ ignores: ['dist'] }, prettier, {
|
|||||||
'@stylistic/js/quotes': ['error', 'single'],
|
'@stylistic/js/quotes': ['error', 'single'],
|
||||||
'@typescript-eslint/no-explicit-any': ['off']
|
'@typescript-eslint/no-explicit-any': ['off']
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag Graph Viewer</title>
|
<title>Lightrag</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -10,13 +10,19 @@
|
|||||||
"preview": "bunx --bun vite preview"
|
"preview": "bunx --bun vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^9.4.0",
|
"@faker-js/faker": "^9.5.0",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-checkbox": "^1.1.4",
|
"@radix-ui/react-checkbox": "^1.1.4",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-popover": "^1.1.6",
|
"@radix-ui/react-popover": "^1.1.6",
|
||||||
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.1.0",
|
||||||
"@react-sigma/core": "^5.0.2",
|
"@react-sigma/core": "^5.0.2",
|
||||||
"@react-sigma/graph-search": "^5.0.3",
|
"@react-sigma/graph-search": "^5.0.3",
|
||||||
"@react-sigma/layout-circlepack": "^5.0.2",
|
"@react-sigma/layout-circlepack": "^5.0.2",
|
||||||
@@ -28,6 +34,7 @@
|
|||||||
"@react-sigma/minimap": "^5.0.2",
|
"@react-sigma/minimap": "^5.0.2",
|
||||||
"@sigma/edge-curve": "^3.1.0",
|
"@sigma/edge-curve": "^3.1.0",
|
||||||
"@sigma/node-border": "^3.0.0",
|
"@sigma/node-border": "^3.0.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
@@ -37,9 +44,13 @@
|
|||||||
"minisearch": "^7.1.1",
|
"minisearch": "^7.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-dropzone": "^14.3.5",
|
||||||
|
"react-number-format": "^5.4.3",
|
||||||
"seedrandom": "^3.0.5",
|
"seedrandom": "^3.0.5",
|
||||||
"sigma": "^3.0.1",
|
"sigma": "^3.0.1",
|
||||||
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.1",
|
||||||
|
"tailwind-scrollbar": "^4.0.0",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -47,19 +58,19 @@
|
|||||||
"@stylistic/eslint-plugin-js": "^3.1.0",
|
"@stylistic/eslint-plugin-js": "^3.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.6",
|
"@tailwindcss/vite": "^4.0.6",
|
||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.13.4",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
"eslint": "^9.20.0",
|
"eslint": "^9.20.1",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.15.0",
|
||||||
"graphology-types": "^0.24.8",
|
"graphology-types": "^0.24.8",
|
||||||
"prettier": "^3.5.0",
|
"prettier": "^3.5.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
@@ -1,17 +1,30 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
import ThemeProvider from '@/components/ThemeProvider'
|
import ThemeProvider from '@/components/ThemeProvider'
|
||||||
import MessageAlert from '@/components/MessageAlert'
|
import MessageAlert from '@/components/MessageAlert'
|
||||||
import StatusIndicator from '@/components/StatusIndicator'
|
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
||||||
import GraphViewer from '@/GraphViewer'
|
import StatusIndicator from '@/components/graph/StatusIndicator'
|
||||||
import { healthCheckInterval } from '@/lib/constants'
|
import { healthCheckInterval } from '@/lib/constants'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { Toaster } from 'sonner'
|
||||||
|
import SiteHeader from '@/features/SiteHeader'
|
||||||
|
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
||||||
|
|
||||||
|
import GraphViewer from '@/features/GraphViewer'
|
||||||
|
import DocumentManager from '@/features/DocumentManager'
|
||||||
|
import RetrievalTesting from '@/features/RetrievalTesting'
|
||||||
|
import ApiSite from '@/features/ApiSite'
|
||||||
|
|
||||||
|
import { Tabs, TabsContent } from '@/components/ui/Tabs'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const message = useBackendState.use.message()
|
const message = useBackendState.use.message()
|
||||||
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||||
|
const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
|
||||||
|
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
||||||
|
|
||||||
// health check
|
// Health check
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableHealthCheck) return
|
if (!enableHealthCheck) return
|
||||||
|
|
||||||
@@ -24,13 +37,50 @@ function App() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [enableHealthCheck])
|
}, [enableHealthCheck])
|
||||||
|
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (message) {
|
||||||
|
if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) {
|
||||||
|
setApiKeyInvalid(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setApiKeyInvalid(false)
|
||||||
|
}, [message, setApiKeyInvalid])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<div className="h-screen w-screen">
|
<main className="flex h-screen w-screen overflow-x-hidden">
|
||||||
|
<Tabs
|
||||||
|
defaultValue={currentTab}
|
||||||
|
className="!m-0 flex grow flex-col !p-0"
|
||||||
|
onValueChange={handleTabChange}
|
||||||
|
>
|
||||||
|
<SiteHeader />
|
||||||
|
<div className="relative grow">
|
||||||
|
<TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
|
<DocumentManager />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
<GraphViewer />
|
<GraphViewer />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
|
<RetrievalTesting />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
|
<ApiSite />
|
||||||
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
|
</Tabs>
|
||||||
{enableHealthCheck && <StatusIndicator />}
|
{enableHealthCheck && <StatusIndicator />}
|
||||||
{message !== null && <MessageAlert />}
|
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
||||||
|
{apiKeyInvalid && <ApiKeyAlert />}
|
||||||
|
<Toaster />
|
||||||
|
</main>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import axios, { AxiosError } from 'axios'
|
||||||
import { backendBaseUrl } from '@/lib/constants'
|
import { backendBaseUrl } from '@/lib/constants'
|
||||||
import { errorMessage } from '@/lib/utils'
|
import { errorMessage } from '@/lib/utils'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
@@ -26,8 +27,6 @@ export type LightragStatus = {
|
|||||||
status: 'healthy'
|
status: 'healthy'
|
||||||
working_directory: string
|
working_directory: string
|
||||||
input_directory: string
|
input_directory: string
|
||||||
indexed_files: string[]
|
|
||||||
indexed_files_count: number
|
|
||||||
configuration: {
|
configuration: {
|
||||||
llm_binding: string
|
llm_binding: string
|
||||||
llm_binding_host: string
|
llm_binding_host: string
|
||||||
@@ -51,94 +50,133 @@ export type LightragDocumentsScanProgress = {
|
|||||||
progress: number
|
progress: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the retrieval mode:
|
||||||
|
* - "naive": Performs a basic search without advanced techniques.
|
||||||
|
* - "local": Focuses on context-dependent information.
|
||||||
|
* - "global": Utilizes global knowledge.
|
||||||
|
* - "hybrid": Combines local and global retrieval methods.
|
||||||
|
* - "mix": Integrates knowledge graph and vector retrieval.
|
||||||
|
*/
|
||||||
export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
|
export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
role: 'user' | 'assistant' | 'system'
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryRequest = {
|
export type QueryRequest = {
|
||||||
query: string
|
query: string
|
||||||
|
/** Specifies the retrieval mode. */
|
||||||
mode: QueryMode
|
mode: QueryMode
|
||||||
stream?: boolean
|
/** If True, only returns the retrieved context without generating a response. */
|
||||||
only_need_context?: boolean
|
only_need_context?: boolean
|
||||||
|
/** If True, only returns the generated prompt without producing a response. */
|
||||||
|
only_need_prompt?: boolean
|
||||||
|
/** Defines the response format. Examples: 'Multiple Paragraphs', 'Single Paragraph', 'Bullet Points'. */
|
||||||
|
response_type?: string
|
||||||
|
/** If True, enables streaming output for real-time responses. */
|
||||||
|
stream?: boolean
|
||||||
|
/** Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode. */
|
||||||
|
top_k?: number
|
||||||
|
/** Maximum number of tokens allowed for each retrieved text chunk. */
|
||||||
|
max_token_for_text_unit?: number
|
||||||
|
/** Maximum number of tokens allocated for relationship descriptions in global retrieval. */
|
||||||
|
max_token_for_global_context?: number
|
||||||
|
/** Maximum number of tokens allocated for entity descriptions in local retrieval. */
|
||||||
|
max_token_for_local_context?: number
|
||||||
|
/** List of high-level keywords to prioritize in retrieval. */
|
||||||
|
hl_keywords?: string[]
|
||||||
|
/** List of low-level keywords to refine retrieval focus. */
|
||||||
|
ll_keywords?: string[]
|
||||||
|
/**
|
||||||
|
* Stores past conversation history to maintain context.
|
||||||
|
* Format: [{"role": "user/assistant", "content": "message"}].
|
||||||
|
*/
|
||||||
|
conversation_history?: Message[]
|
||||||
|
/** Number of complete conversation turns (user-assistant pairs) to consider in the response context. */
|
||||||
|
history_turns?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryResponse = {
|
export type QueryResponse = {
|
||||||
response: string
|
response: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DocActionResponse = {
|
||||||
|
status: 'success' | 'partial_success' | 'failure'
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed'
|
||||||
|
|
||||||
|
export type DocStatusResponse = {
|
||||||
|
id: string
|
||||||
|
content_summary: string
|
||||||
|
content_length: number
|
||||||
|
status: DocStatus
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
chunks_count?: number
|
||||||
|
error?: string
|
||||||
|
metadata?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DocsStatusesResponse = {
|
||||||
|
statuses: Record<DocStatus, DocStatusResponse[]>
|
||||||
|
}
|
||||||
|
|
||||||
export const InvalidApiKeyError = 'Invalid API Key'
|
export const InvalidApiKeyError = 'Invalid API Key'
|
||||||
export const RequireApiKeError = 'API Key required'
|
export const RequireApiKeError = 'API Key required'
|
||||||
|
|
||||||
// Helper functions
|
// Axios instance
|
||||||
const getResponseContent = async (response: Response) => {
|
const axiosInstance = axios.create({
|
||||||
const contentType = response.headers.get('content-type')
|
baseURL: backendBaseUrl,
|
||||||
if (contentType) {
|
headers: {
|
||||||
if (contentType.includes('application/json')) {
|
'Content-Type': 'application/json'
|
||||||
const data = await response.json()
|
|
||||||
return JSON.stringify(data, undefined, 2)
|
|
||||||
} else if (contentType.startsWith('text/')) {
|
|
||||||
return await response.text()
|
|
||||||
} else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
|
|
||||||
return await response.text()
|
|
||||||
} else if (contentType.includes('application/octet-stream')) {
|
|
||||||
const buffer = await response.arrayBuffer()
|
|
||||||
const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true })
|
|
||||||
return decoder.decode(buffer)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return await response.text()
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to decode as text, may be binary:', error)
|
|
||||||
return `[Could not decode response body. Content-Type: ${contentType}]`
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return await response.text()
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to decode as text, may be binary:', error)
|
|
||||||
return '[Could not decode response body. No Content-Type header.]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise<Response> => {
|
// Interceptor:add api key
|
||||||
|
axiosInstance.interceptors.request.use((config) => {
|
||||||
const apiKey = useSettingsStore.getState().apiKey
|
const apiKey = useSettingsStore.getState().apiKey
|
||||||
const headers = {
|
if (apiKey) {
|
||||||
...(options.headers || {}),
|
config.headers['X-API-Key'] = apiKey
|
||||||
...(apiKey ? { 'X-API-Key': apiKey } : {})
|
|
||||||
}
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
const response = await fetch(backendBaseUrl + url, {
|
// Interceptor:hanle error
|
||||||
...options,
|
axiosInstance.interceptors.response.use(
|
||||||
headers
|
(response) => response,
|
||||||
})
|
(error: AxiosError) => {
|
||||||
|
if (error.response) {
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${response.status} ${response.statusText}\n${await getResponseContent(response)}\n${response.url}`
|
`${error.response.status} ${error.response.statusText}\n${JSON.stringify(
|
||||||
|
error.response.data
|
||||||
|
)}\n${error.config?.url}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
throw error
|
||||||
return response
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
// API methods
|
// API methods
|
||||||
export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
|
export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
|
||||||
const response = await fetchWithAuth(`/graphs?label=${label}`)
|
const response = await axiosInstance.get(`/graphs?label=${label}`)
|
||||||
return await response.json()
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGraphLabels = async (): Promise<string[]> => {
|
export const getGraphLabels = async (): Promise<string[]> => {
|
||||||
const response = await fetchWithAuth('/graph/label/list')
|
const response = await axiosInstance.get('/graph/label/list')
|
||||||
return await response.json()
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkHealth = async (): Promise<
|
export const checkHealth = async (): Promise<
|
||||||
LightragStatus | { status: 'error'; message: string }
|
LightragStatus | { status: 'error'; message: string }
|
||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetchWithAuth('/health')
|
const response = await axiosInstance.get('/health')
|
||||||
return await response.json()
|
return response.data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
@@ -147,132 +185,132 @@ export const checkHealth = async (): Promise<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDocuments = async (): Promise<string[]> => {
|
export const getDocuments = async (): Promise<DocsStatusesResponse> => {
|
||||||
const response = await fetchWithAuth('/documents')
|
const response = await axiosInstance.get('/documents')
|
||||||
return await response.json()
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scanNewDocuments = async (): Promise<{ status: string }> => {
|
||||||
|
const response = await axiosInstance.post('/documents/scan')
|
||||||
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
|
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
|
||||||
const response = await fetchWithAuth('/documents/scan-progress')
|
const response = await axiosInstance.get('/documents/scan-progress')
|
||||||
return await response.json()
|
return response.data
|
||||||
}
|
|
||||||
|
|
||||||
export const uploadDocument = async (
|
|
||||||
file: File
|
|
||||||
): Promise<{
|
|
||||||
status: string
|
|
||||||
message: string
|
|
||||||
total_documents: number
|
|
||||||
}> => {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
|
|
||||||
const response = await fetchWithAuth('/documents/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const startDocumentScan = async (): Promise<{ status: string }> => {
|
|
||||||
const response = await fetchWithAuth('/documents/scan', {
|
|
||||||
method: 'POST'
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryText = async (request: QueryRequest): Promise<QueryResponse> => {
|
export const queryText = async (request: QueryRequest): Promise<QueryResponse> => {
|
||||||
const response = await fetchWithAuth('/query', {
|
const response = await axiosInstance.post('/query', request)
|
||||||
method: 'POST',
|
return response.data
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(request)
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => {
|
export const queryTextStream = async (
|
||||||
const response = await fetchWithAuth('/query/stream', {
|
request: QueryRequest,
|
||||||
method: 'POST',
|
onChunk: (chunk: string) => void,
|
||||||
headers: {
|
onError?: (error: string) => void
|
||||||
'Content-Type': 'application/json'
|
) => {
|
||||||
},
|
|
||||||
body: JSON.stringify(request)
|
|
||||||
})
|
|
||||||
|
|
||||||
const reader = response.body?.getReader()
|
|
||||||
if (!reader) throw new Error('No response body')
|
|
||||||
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
const chunk = decoder.decode(value)
|
|
||||||
const lines = chunk.split('\n')
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line) {
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(line)
|
let buffer = ''
|
||||||
if (data.response) {
|
await axiosInstance.post('/query/stream', request, {
|
||||||
onChunk(data.response)
|
responseType: 'text',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/x-ndjson'
|
||||||
|
},
|
||||||
|
transformResponse: [
|
||||||
|
(data: string) => {
|
||||||
|
// Accumulate the data and process complete lines
|
||||||
|
buffer += data
|
||||||
|
const lines = buffer.split('\n')
|
||||||
|
// Keep the last potentially incomplete line in the buffer
|
||||||
|
buffer = lines.pop() || ''
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line)
|
||||||
|
if (parsed.response) {
|
||||||
|
onChunk(parsed.response)
|
||||||
|
} else if (parsed.error && onError) {
|
||||||
|
onError(parsed.error)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing stream chunk:', e)
|
console.error('Error parsing stream chunk:', e)
|
||||||
|
if (onError) onError('Error parsing server response')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process any remaining data in the buffer
|
||||||
|
if (buffer.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(buffer)
|
||||||
|
if (parsed.response) {
|
||||||
|
onChunk(parsed.response)
|
||||||
|
} else if (parsed.error && onError) {
|
||||||
|
onError(parsed.error)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing final chunk:', e)
|
||||||
|
if (onError) onError('Error parsing server response')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = errorMessage(error)
|
||||||
|
console.error('Stream request failed:', message)
|
||||||
|
if (onError) onError(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text insertion API
|
|
||||||
export const insertText = async (
|
export const insertText = async (
|
||||||
text: string,
|
text: string,
|
||||||
description?: string
|
description?: string
|
||||||
): Promise<{
|
): Promise<DocActionResponse> => {
|
||||||
status: string
|
const response = await axiosInstance.post('/documents/text', { text, description })
|
||||||
message: string
|
return response.data
|
||||||
document_count: number
|
|
||||||
}> => {
|
|
||||||
const response = await fetchWithAuth('/documents/text', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ text, description })
|
|
||||||
})
|
|
||||||
return await response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch file upload API
|
export const uploadDocument = async (
|
||||||
export const uploadBatchDocuments = async (
|
file: File,
|
||||||
files: File[]
|
onUploadProgress?: (percentCompleted: number) => void
|
||||||
): Promise<{
|
): Promise<DocActionResponse> => {
|
||||||
status: string
|
|
||||||
message: string
|
|
||||||
document_count: number
|
|
||||||
}> => {
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
files.forEach((file) => {
|
formData.append('file', file)
|
||||||
formData.append('files', file)
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await fetchWithAuth('/documents/batch', {
|
const response = await axiosInstance.post('/documents/upload', formData, {
|
||||||
method: 'POST',
|
headers: {
|
||||||
body: formData
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
// prettier-ignore
|
||||||
|
onUploadProgress:
|
||||||
|
onUploadProgress !== undefined
|
||||||
|
? (progressEvent) => {
|
||||||
|
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total!)
|
||||||
|
onUploadProgress(percentCompleted)
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
})
|
})
|
||||||
return await response.json()
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all documents API
|
export const batchUploadDocuments = async (
|
||||||
export const clearDocuments = async (): Promise<{
|
files: File[],
|
||||||
status: string
|
onUploadProgress?: (fileName: string, percentCompleted: number) => void
|
||||||
message: string
|
): Promise<DocActionResponse[]> => {
|
||||||
document_count: number
|
return await Promise.all(
|
||||||
}> => {
|
files.map(async (file) => {
|
||||||
const response = await fetchWithAuth('/documents', {
|
return await uploadDocument(file, (percentCompleted) => {
|
||||||
method: 'DELETE'
|
onUploadProgress?.(file.name, percentCompleted)
|
||||||
})
|
})
|
||||||
return await response.json()
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearDocuments = async (): Promise<DocActionResponse> => {
|
||||||
|
const response = await axiosInstance.delete('/documents')
|
||||||
|
return response.data
|
||||||
}
|
}
|
||||||
|
77
lightrag_webui/src/components/ApiKeyAlert.tsx
Normal file
77
lightrag_webui/src/components/ApiKeyAlert.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from '@/components/ui/AlertDialog'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Input from '@/components/ui/Input'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useBackendState } from '@/stores/state'
|
||||||
|
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
||||||
|
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
const ApiKeyAlert = () => {
|
||||||
|
const [opened, setOpened] = useState<boolean>(true)
|
||||||
|
const apiKey = useSettingsStore.use.apiKey()
|
||||||
|
const [tempApiKey, setTempApiKey] = useState<string>('')
|
||||||
|
const message = useBackendState.use.message()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTempApiKey(apiKey || '')
|
||||||
|
}, [apiKey, opened])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (message) {
|
||||||
|
if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) {
|
||||||
|
setOpened(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [message, setOpened])
|
||||||
|
|
||||||
|
const setApiKey = useCallback(async () => {
|
||||||
|
useSettingsStore.setState({ apiKey: tempApiKey || null })
|
||||||
|
if (await useBackendState.getState().check()) {
|
||||||
|
setOpened(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.error('API Key is invalid')
|
||||||
|
}, [tempApiKey])
|
||||||
|
|
||||||
|
const handleTempApiKeyChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTempApiKey(e.target.value)
|
||||||
|
},
|
||||||
|
[setTempApiKey]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={opened} onOpenChange={setOpened}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>API Key is required</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>Please enter your API key</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<form className="flex gap-2" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={tempApiKey}
|
||||||
|
onChange={handleTempApiKeyChange}
|
||||||
|
placeholder="Enter your API key"
|
||||||
|
className="max-h-full w-full min-w-0"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={setApiKey} variant="outline" size="sm">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiKeyAlert
|
@@ -22,10 +22,11 @@ const MessageAlert = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
variant={health ? 'default' : 'destructive'}
|
// variant={health ? 'default' : 'destructive'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background/90 absolute top-2 left-1/2 flex w-auto -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
|
'bg-background/90 absolute top-12 left-1/2 flex w-auto max-w-lg -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
|
||||||
isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0'
|
isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0',
|
||||||
|
!health && 'bg-red-700 text-white'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!health && (
|
{!health && (
|
||||||
@@ -42,7 +43,7 @@ const MessageAlert = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
className="text-primary max-h-8 border !p-2 text-xs"
|
className="border-primary max-h-8 border !p-2 text-xs"
|
||||||
onClick={() => useBackendState.getState().clear()}
|
onClick={() => useBackendState.getState().clear()}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
|
@@ -19,6 +19,7 @@ export default function ThemeToggle() {
|
|||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
tooltip="Switch to light theme"
|
tooltip="Switch to light theme"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
side="bottom"
|
||||||
>
|
>
|
||||||
<MoonIcon />
|
<MoonIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -30,6 +31,7 @@ export default function ThemeToggle() {
|
|||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
tooltip="Switch to dark theme"
|
tooltip="Switch to dark theme"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
side="bottom"
|
||||||
>
|
>
|
||||||
<SunIcon />
|
<SunIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -0,0 +1,52 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from '@/components/ui/Dialog'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { errorMessage } from '@/lib/utils'
|
||||||
|
import { clearDocuments } from '@/api/lightrag'
|
||||||
|
|
||||||
|
import { EraserIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function ClearDocumentsDialog() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const handleClear = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await clearDocuments()
|
||||||
|
if (result.status === 'success') {
|
||||||
|
toast.success('Documents cleared successfully')
|
||||||
|
setOpen(false)
|
||||||
|
} else {
|
||||||
|
toast.error(`Clear Documents Failed:\n${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Clear Documents Failed:\n' + errorMessage(err))
|
||||||
|
}
|
||||||
|
}, [setOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
|
||||||
|
<EraserIcon/> Clear
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Clear documents</DialogTitle>
|
||||||
|
<DialogDescription>Do you really want to clear all documents?</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Button variant="destructive" onClick={handleClear}>
|
||||||
|
YES
|
||||||
|
</Button>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,91 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from '@/components/ui/Dialog'
|
||||||
|
import FileUploader from '@/components/ui/FileUploader'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { errorMessage } from '@/lib/utils'
|
||||||
|
import { uploadDocument } from '@/api/lightrag'
|
||||||
|
|
||||||
|
import { UploadIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function UploadDocumentsDialog() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
|
const [progresses, setProgresses] = useState<Record<string, number>>({})
|
||||||
|
|
||||||
|
const handleDocumentsUpload = useCallback(
|
||||||
|
async (filesToUpload: File[]) => {
|
||||||
|
setIsUploading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
filesToUpload.map(async (file) => {
|
||||||
|
try {
|
||||||
|
const result = await uploadDocument(file, (percentCompleted: number) => {
|
||||||
|
console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
|
||||||
|
setProgresses((pre) => ({
|
||||||
|
...pre,
|
||||||
|
[file.name]: percentCompleted
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
if (result.status === 'success') {
|
||||||
|
toast.success(`Upload Success:\n${file.name} uploaded successfully`)
|
||||||
|
} else {
|
||||||
|
toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Upload Failed\n' + errorMessage(err))
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false)
|
||||||
|
// setOpen(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setIsUploading, setProgresses]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (isUploading && !open) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOpen(open)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="default" side="bottom" tooltip="Upload documents" size="sm">
|
||||||
|
<UploadIcon /> Upload
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Upload documents</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Drag and drop your documents here or click to browse.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FileUploader
|
||||||
|
maxFileCount={Infinity}
|
||||||
|
maxSize={200 * 1024 * 1024}
|
||||||
|
description="supported types: TXT, MD, DOC, PDF, PPTX"
|
||||||
|
onUpload={handleDocumentsUpload}
|
||||||
|
progresses={progresses}
|
||||||
|
disabled={isUploading}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||||
import { Checkbox } from '@/components/ui/Checkbox'
|
import Checkbox from '@/components/ui/Checkbox'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import Separator from '@/components/ui/Separator'
|
import Separator from '@/components/ui/Separator'
|
||||||
import Input from '@/components/ui/Input'
|
import Input from '@/components/ui/Input'
|
||||||
@@ -40,7 +40,7 @@ const LabeledCheckBox = ({
|
|||||||
*/
|
*/
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const [opened, setOpened] = useState<boolean>(false)
|
const [opened, setOpened] = useState<boolean>(false)
|
||||||
const [tempApiKey, setTempApiKey] = useState<string>('') // 用于临时存储输入的API Key
|
const [tempApiKey, setTempApiKey] = useState<string>('')
|
||||||
|
|
||||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||||
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
@@ -14,8 +14,6 @@ const StatusCard = ({ status }: { status: LightragStatus | null }) => {
|
|||||||
<span className="truncate">{status.working_directory}</span>
|
<span className="truncate">{status.working_directory}</span>
|
||||||
<span>Input Directory:</span>
|
<span>Input Directory:</span>
|
||||||
<span className="truncate">{status.input_directory}</span>
|
<span className="truncate">{status.input_directory}</span>
|
||||||
<span>Indexed Files:</span>
|
|
||||||
<span>{status.indexed_files_count}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'
|
|||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||||
import StatusCard from '@/components/StatusCard'
|
import StatusCard from '@/components/graph/StatusCard'
|
||||||
|
|
||||||
const StatusIndicator = () => {
|
const StatusIndicator = () => {
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
279
lightrag_webui/src/components/retrieval/QuerySettings.tsx
Normal file
279
lightrag_webui/src/components/retrieval/QuerySettings.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { QueryMode, QueryRequest } from '@/api/lightrag'
|
||||||
|
import Text from '@/components/ui/Text'
|
||||||
|
import Input from '@/components/ui/Input'
|
||||||
|
import Checkbox from '@/components/ui/Checkbox'
|
||||||
|
import NumberInput from '@/components/ui/NumberInput'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@/components/ui/Select'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
|
export default function QuerySettings() {
|
||||||
|
const querySettings = useSettingsStore((state) => state.querySettings)
|
||||||
|
|
||||||
|
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
|
||||||
|
useSettingsStore.getState().updateQuerySettings({ [key]: value })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="flex shrink-0 flex-col">
|
||||||
|
<CardHeader className="px-4 pt-4 pb-2">
|
||||||
|
<CardTitle>Parameters</CardTitle>
|
||||||
|
<CardDescription>Configure your query parameters</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
||||||
|
<div className="relative size-full">
|
||||||
|
<div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2">
|
||||||
|
{/* Query Mode */}
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Query Mode"
|
||||||
|
tooltip="Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={querySettings.mode}
|
||||||
|
onValueChange={(v) => handleChange('mode', v as QueryMode)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="naive">Naive</SelectItem>
|
||||||
|
<SelectItem value="local">Local</SelectItem>
|
||||||
|
<SelectItem value="global">Global</SelectItem>
|
||||||
|
<SelectItem value="hybrid">Hybrid</SelectItem>
|
||||||
|
<SelectItem value="mix">Mix</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Response Format */}
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Response Format"
|
||||||
|
tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={querySettings.response_type}
|
||||||
|
onValueChange={(v) => handleChange('response_type', v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem>
|
||||||
|
<SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
|
||||||
|
<SelectItem value="Bullet Points">Bullet Points</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Top K */}
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Top K Results"
|
||||||
|
tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
id="top_k"
|
||||||
|
stepper={1}
|
||||||
|
value={querySettings.top_k}
|
||||||
|
onValueChange={(v) => handleChange('top_k', v)}
|
||||||
|
min={1}
|
||||||
|
placeholder="Number of results"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Max Tokens */}
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Max Tokens for Text Unit"
|
||||||
|
tooltip="Maximum number of tokens allowed for each retrieved text chunk"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
id="max_token_for_text_unit"
|
||||||
|
stepper={500}
|
||||||
|
value={querySettings.max_token_for_text_unit}
|
||||||
|
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
|
||||||
|
min={1}
|
||||||
|
placeholder="Max tokens for text unit"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
text="Max Tokens for Global Context"
|
||||||
|
tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
id="max_token_for_global_context"
|
||||||
|
stepper={500}
|
||||||
|
value={querySettings.max_token_for_global_context}
|
||||||
|
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
|
||||||
|
min={1}
|
||||||
|
placeholder="Max tokens for global context"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Max Tokens for Local Context"
|
||||||
|
tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
id="max_token_for_local_context"
|
||||||
|
stepper={500}
|
||||||
|
value={querySettings.max_token_for_local_context}
|
||||||
|
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
|
||||||
|
min={1}
|
||||||
|
placeholder="Max tokens for local context"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* History Turns */}
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="History Turns"
|
||||||
|
tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
className="!border-input"
|
||||||
|
id="history_turns"
|
||||||
|
stepper={1}
|
||||||
|
type="text"
|
||||||
|
value={querySettings.history_turns}
|
||||||
|
onValueChange={(v) => handleChange('history_turns', v)}
|
||||||
|
min={0}
|
||||||
|
placeholder="Number of history turns"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Keywords */}
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="High-Level Keywords"
|
||||||
|
tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="hl_keywords"
|
||||||
|
type="text"
|
||||||
|
value={querySettings.hl_keywords?.join(', ')}
|
||||||
|
onChange={(e) => {
|
||||||
|
const keywords = e.target.value
|
||||||
|
.split(',')
|
||||||
|
.map((k) => k.trim())
|
||||||
|
.filter((k) => k !== '')
|
||||||
|
handleChange('hl_keywords', keywords)
|
||||||
|
}}
|
||||||
|
placeholder="Enter keywords"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Low-Level Keywords"
|
||||||
|
tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="ll_keywords"
|
||||||
|
type="text"
|
||||||
|
value={querySettings.ll_keywords?.join(', ')}
|
||||||
|
onChange={(e) => {
|
||||||
|
const keywords = e.target.value
|
||||||
|
.split(',')
|
||||||
|
.map((k) => k.trim())
|
||||||
|
.filter((k) => k !== '')
|
||||||
|
handleChange('ll_keywords', keywords)
|
||||||
|
}}
|
||||||
|
placeholder="Enter keywords"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
|
||||||
|
{/* Toggle Options */}
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Only Need Context"
|
||||||
|
tooltip="If True, only returns the retrieved context without generating a response"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<div className="grow" />
|
||||||
|
<Checkbox
|
||||||
|
className="mr-1 cursor-pointer"
|
||||||
|
id="only_need_context"
|
||||||
|
checked={querySettings.only_need_context}
|
||||||
|
onCheckedChange={(checked) => handleChange('only_need_context', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Only Need Prompt"
|
||||||
|
tooltip="If True, only returns the generated prompt without producing a response"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<div className="grow" />
|
||||||
|
<Checkbox
|
||||||
|
className="mr-1 cursor-pointer"
|
||||||
|
id="only_need_prompt"
|
||||||
|
checked={querySettings.only_need_prompt}
|
||||||
|
onCheckedChange={(checked) => handleChange('only_need_prompt', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Text
|
||||||
|
className="ml-1"
|
||||||
|
text="Stream Response"
|
||||||
|
tooltip="If True, enables streaming output for real-time responses"
|
||||||
|
side="left"
|
||||||
|
/>
|
||||||
|
<div className="grow" />
|
||||||
|
<Checkbox
|
||||||
|
className="mr-1 cursor-pointer"
|
||||||
|
id="stream"
|
||||||
|
checked={querySettings.stream}
|
||||||
|
onCheckedChange={(checked) => handleChange('stream', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
115
lightrag_webui/src/components/ui/AlertDialog.tsx
Normal file
115
lightrag_webui/src/components/ui/AlertDialog.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { buttonVariants } from '@/components/ui/Button'
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = 'AlertDialogHeader'
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = 'AlertDialogFooter'
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-lg font-semibold', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel
|
||||||
|
}
|
@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
|
<CommandList hidden={!open || debouncedSearchTerm.length === 0}>
|
||||||
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
||||||
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
|
33
lightrag_webui/src/components/ui/Badge.tsx
Normal file
33
lightrag_webui/src/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
||||||
|
secondary:
|
||||||
|
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
destructive:
|
||||||
|
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||||
|
outline: 'text-foreground'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Badge
|
@@ -4,7 +4,8 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
|||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
|
55
lightrag_webui/src/components/ui/Card.tsx
Normal file
55
lightrag_webui/src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn('bg-card text-card-foreground rounded-xl border shadow', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Card.displayName = 'Card'
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardHeader.displayName = 'CardHeader'
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn('leading-none font-semibold tracking-tight', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardTitle.displayName = 'CardTitle'
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn('text-muted-foreground text-sm', className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardDescription.displayName = 'CardDescription'
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardContent.displayName = 'CardContent'
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardFooter.displayName = 'CardFooter'
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
@@ -23,4 +23,4 @@ const Checkbox = React.forwardRef<
|
|||||||
))
|
))
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
export { Checkbox }
|
export default Checkbox
|
||||||
|
64
lightrag_webui/src/components/ui/DataTable.tsx
Normal file
64
lightrag_webui/src/components/ui/DataTable.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@/components/ui/Table'
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[]
|
||||||
|
data: TData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -65,7 +65,7 @@ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
|
|||||||
DialogFooter.displayName = 'DialogFooter'
|
DialogFooter.displayName = 'DialogFooter'
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
React.ComponentRef<typeof DialogPrimitive.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
@@ -77,7 +77,7 @@ const DialogTitle = React.forwardRef<
|
|||||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
const DialogDescription = React.forwardRef<
|
const DialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
React.ComponentRef<typeof DialogPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
|
38
lightrag_webui/src/components/ui/EmptyCard.tsx
Normal file
38
lightrag_webui/src/components/ui/EmptyCard.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Card, CardDescription, CardTitle } from '@/components/ui/Card'
|
||||||
|
import { FilesIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
interface EmptyCardProps extends React.ComponentPropsWithoutRef<typeof Card> {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
action?: React.ReactNode
|
||||||
|
icon?: React.ComponentType<{ className?: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmptyCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon: Icon = FilesIcon,
|
||||||
|
action,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: EmptyCardProps) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
'flex w-full flex-col items-center justify-center space-y-6 bg-transparent p-16',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="mr-4 shrink-0 rounded-full border border-dashed p-4">
|
||||||
|
<Icon className="text-muted-foreground size-8" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-1.5 text-center">
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
{description ? <CardDescription>{description}</CardDescription> : null}
|
||||||
|
</div>
|
||||||
|
{action ? action : null}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
322
lightrag_webui/src/components/ui/FileUploader.tsx
Normal file
322
lightrag_webui/src/components/ui/FileUploader.tsx
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
/**
|
||||||
|
* @see https://github.com/sadmann7/file-uploader
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import { FileText, Upload, X } from 'lucide-react'
|
||||||
|
import Dropzone, { type DropzoneProps, type FileRejection } from 'react-dropzone'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useControllableState } from '@radix-ui/react-use-controllable-state'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Progress from '@/components/ui/Progress'
|
||||||
|
import { ScrollArea } from '@/components/ui/ScrollArea'
|
||||||
|
import { supportedFileTypes } from '@/lib/constants'
|
||||||
|
|
||||||
|
interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
/**
|
||||||
|
* Value of the uploader.
|
||||||
|
* @type File[]
|
||||||
|
* @default undefined
|
||||||
|
* @example value={files}
|
||||||
|
*/
|
||||||
|
value?: File[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be called when the value changes.
|
||||||
|
* @type (files: File[]) => void
|
||||||
|
* @default undefined
|
||||||
|
* @example onValueChange={(files) => setFiles(files)}
|
||||||
|
*/
|
||||||
|
onValueChange?: (files: File[]) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be called when files are uploaded.
|
||||||
|
* @type (files: File[]) => Promise<void>
|
||||||
|
* @default undefined
|
||||||
|
* @example onUpload={(files) => uploadFiles(files)}
|
||||||
|
*/
|
||||||
|
onUpload?: (files: File[]) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress of the uploaded files.
|
||||||
|
* @type Record<string, number> | undefined
|
||||||
|
* @default undefined
|
||||||
|
* @example progresses={{ "file1.png": 50 }}
|
||||||
|
*/
|
||||||
|
progresses?: Record<string, number>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepted file types for the uploader.
|
||||||
|
* @type { [key: string]: string[]}
|
||||||
|
* @default
|
||||||
|
* ```ts
|
||||||
|
* { "text/*": [] }
|
||||||
|
* ```
|
||||||
|
* @example accept={["text/plain", "application/pdf"]}
|
||||||
|
*/
|
||||||
|
accept?: DropzoneProps['accept']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum file size for the uploader.
|
||||||
|
* @type number | undefined
|
||||||
|
* @default 1024 * 1024 * 200 // 200MB
|
||||||
|
* @example maxSize={1024 * 1024 * 2} // 2MB
|
||||||
|
*/
|
||||||
|
maxSize?: DropzoneProps['maxSize']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of files for the uploader.
|
||||||
|
* @type number | undefined
|
||||||
|
* @default 1
|
||||||
|
* @example maxFileCount={4}
|
||||||
|
*/
|
||||||
|
maxFileCount?: DropzoneProps['maxFiles']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the uploader should accept multiple files.
|
||||||
|
* @type boolean
|
||||||
|
* @default false
|
||||||
|
* @example multiple
|
||||||
|
*/
|
||||||
|
multiple?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the uploader is disabled.
|
||||||
|
* @type boolean
|
||||||
|
* @default false
|
||||||
|
* @example disabled
|
||||||
|
*/
|
||||||
|
disabled?: boolean
|
||||||
|
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(
|
||||||
|
bytes: number,
|
||||||
|
opts: {
|
||||||
|
decimals?: number
|
||||||
|
sizeType?: 'accurate' | 'normal'
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const { decimals = 0, sizeType = 'normal' } = opts
|
||||||
|
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB']
|
||||||
|
if (bytes === 0) return '0 Byte'
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${
|
||||||
|
sizeType === 'accurate' ? (accurateSizes[i] ?? 'Bytes') : (sizes[i] ?? 'Bytes')
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileUploader(props: FileUploaderProps) {
|
||||||
|
const {
|
||||||
|
value: valueProp,
|
||||||
|
onValueChange,
|
||||||
|
onUpload,
|
||||||
|
progresses,
|
||||||
|
accept = supportedFileTypes,
|
||||||
|
maxSize = 1024 * 1024 * 200,
|
||||||
|
maxFileCount = 1,
|
||||||
|
multiple = false,
|
||||||
|
disabled = false,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
...dropzoneProps
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const [files, setFiles] = useControllableState({
|
||||||
|
prop: valueProp,
|
||||||
|
onChange: onValueChange
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDrop = React.useCallback(
|
||||||
|
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||||
|
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
|
||||||
|
toast.error('Cannot upload more than 1 file at a time')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((files?.length ?? 0) + acceptedFiles.length > maxFileCount) {
|
||||||
|
toast.error(`Cannot upload more than ${maxFileCount} files`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFiles = acceptedFiles.map((file) =>
|
||||||
|
Object.assign(file, {
|
||||||
|
preview: URL.createObjectURL(file)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedFiles = files ? [...files, ...newFiles] : newFiles
|
||||||
|
|
||||||
|
setFiles(updatedFiles)
|
||||||
|
|
||||||
|
if (rejectedFiles.length > 0) {
|
||||||
|
rejectedFiles.forEach(({ file }) => {
|
||||||
|
toast.error(`File ${file.name} was rejected`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onUpload && updatedFiles.length > 0 && updatedFiles.length <= maxFileCount) {
|
||||||
|
const target = updatedFiles.length > 0 ? `${updatedFiles.length} files` : 'file'
|
||||||
|
|
||||||
|
toast.promise(onUpload(updatedFiles), {
|
||||||
|
loading: `Uploading ${target}...`,
|
||||||
|
success: () => {
|
||||||
|
setFiles([])
|
||||||
|
return `${target} uploaded`
|
||||||
|
},
|
||||||
|
error: `Failed to upload ${target}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
[files, maxFileCount, multiple, onUpload, setFiles]
|
||||||
|
)
|
||||||
|
|
||||||
|
function onRemove(index: number) {
|
||||||
|
if (!files) return
|
||||||
|
const newFiles = files.filter((_, i) => i !== index)
|
||||||
|
setFiles(newFiles)
|
||||||
|
onValueChange?.(newFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke preview url when component unmounts
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (!files) return
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (isFileWithPreview(file)) {
|
||||||
|
URL.revokeObjectURL(file.preview)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isDisabled = disabled || (files?.length ?? 0) >= maxFileCount
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col gap-6 overflow-hidden">
|
||||||
|
<Dropzone
|
||||||
|
onDrop={onDrop}
|
||||||
|
accept={accept}
|
||||||
|
maxSize={maxSize}
|
||||||
|
maxFiles={maxFileCount}
|
||||||
|
multiple={maxFileCount > 1 || multiple}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={cn(
|
||||||
|
'group border-muted-foreground/25 hover:bg-muted/25 relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed px-5 py-2.5 text-center transition',
|
||||||
|
'ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
||||||
|
isDragActive && 'border-muted-foreground/50',
|
||||||
|
isDisabled && 'pointer-events-none opacity-60',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...dropzoneProps}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{isDragActive ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
|
||||||
|
<div className="rounded-full border border-dashed p-3">
|
||||||
|
<Upload className="text-muted-foreground size-7" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground font-medium">Drop the files here</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
|
||||||
|
<div className="rounded-full border border-dashed p-3">
|
||||||
|
<Upload className="text-muted-foreground size-7" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-px">
|
||||||
|
<p className="text-muted-foreground font-medium">
|
||||||
|
Drag and drop files here, or click to select files
|
||||||
|
</p>
|
||||||
|
{description ? (
|
||||||
|
<p className="text-muted-foreground/70 text-sm">{description}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground/70 text-sm">
|
||||||
|
You can upload
|
||||||
|
{maxFileCount > 1
|
||||||
|
? ` ${maxFileCount === Infinity ? 'multiple' : maxFileCount}
|
||||||
|
files (up to ${formatBytes(maxSize)} each)`
|
||||||
|
: ` a file with ${formatBytes(maxSize)}`}
|
||||||
|
Supported formats: TXT, MD, DOC, PDF, PPTX
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
{files?.length ? (
|
||||||
|
<ScrollArea className="h-fit w-full px-3">
|
||||||
|
<div className="flex max-h-48 flex-col gap-4">
|
||||||
|
{files?.map((file, index) => (
|
||||||
|
<FileCard
|
||||||
|
key={index}
|
||||||
|
file={file}
|
||||||
|
onRemove={() => onRemove(index)}
|
||||||
|
progress={progresses?.[file.name]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileCardProps {
|
||||||
|
file: File
|
||||||
|
onRemove: () => void
|
||||||
|
progress?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileCard({ file, progress, onRemove }: FileCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center gap-2.5">
|
||||||
|
<div className="flex flex-1 gap-2.5">
|
||||||
|
{isFileWithPreview(file) ? <FilePreview file={file} /> : null}
|
||||||
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
<div className="flex flex-col gap-px">
|
||||||
|
<p className="text-foreground/80 line-clamp-1 text-sm font-medium">{file.name}</p>
|
||||||
|
<p className="text-muted-foreground text-xs">{formatBytes(file.size)}</p>
|
||||||
|
</div>
|
||||||
|
{progress ? <Progress value={progress} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button type="button" variant="outline" size="icon" className="size-7" onClick={onRemove}>
|
||||||
|
<X className="size-4" aria-hidden="true" />
|
||||||
|
<span className="sr-only">Remove file</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFileWithPreview(file: File): file is File & { preview: string } {
|
||||||
|
return 'preview' in file && typeof file.preview === 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilePreviewProps {
|
||||||
|
file: File & { preview: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilePreview({ file }: FilePreviewProps) {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
return <div className="aspect-square shrink-0 rounded-md object-cover" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FileText className="text-muted-foreground size-10" aria-hidden="true" />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileUploader
|
131
lightrag_webui/src/components/ui/NumberInput.tsx
Normal file
131
lightrag_webui/src/components/ui/NumberInput.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
|
import { forwardRef, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { NumericFormat, NumericFormatProps } from 'react-number-format'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import Input from '@/components/ui/Input'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export interface NumberInputProps extends Omit<NumericFormatProps, 'value' | 'onValueChange'> {
|
||||||
|
stepper?: number
|
||||||
|
thousandSeparator?: string
|
||||||
|
placeholder?: string
|
||||||
|
defaultValue?: number
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
value?: number // Controlled value
|
||||||
|
suffix?: string
|
||||||
|
prefix?: string
|
||||||
|
onValueChange?: (value: number | undefined) => void
|
||||||
|
fixedDecimalScale?: boolean
|
||||||
|
decimalScale?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
stepper,
|
||||||
|
thousandSeparator,
|
||||||
|
placeholder,
|
||||||
|
defaultValue,
|
||||||
|
min = -Infinity,
|
||||||
|
max = Infinity,
|
||||||
|
onValueChange,
|
||||||
|
fixedDecimalScale = false,
|
||||||
|
decimalScale = 0,
|
||||||
|
className = undefined,
|
||||||
|
suffix,
|
||||||
|
prefix,
|
||||||
|
value: controlledValue,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [value, setValue] = useState<number | undefined>(controlledValue ?? defaultValue)
|
||||||
|
|
||||||
|
const handleIncrement = useCallback(() => {
|
||||||
|
setValue((prev) =>
|
||||||
|
prev === undefined ? (stepper ?? 1) : Math.min(prev + (stepper ?? 1), max)
|
||||||
|
)
|
||||||
|
}, [stepper, max])
|
||||||
|
|
||||||
|
const handleDecrement = useCallback(() => {
|
||||||
|
setValue((prev) =>
|
||||||
|
prev === undefined ? -(stepper ?? 1) : Math.max(prev - (stepper ?? 1), min)
|
||||||
|
)
|
||||||
|
}, [stepper, min])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlledValue !== undefined) {
|
||||||
|
setValue(controlledValue)
|
||||||
|
}
|
||||||
|
}, [controlledValue])
|
||||||
|
|
||||||
|
const handleChange = (values: { value: string; floatValue: number | undefined }) => {
|
||||||
|
const newValue = values.floatValue === undefined ? undefined : values.floatValue
|
||||||
|
setValue(newValue)
|
||||||
|
if (onValueChange) {
|
||||||
|
onValueChange(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
if (value < min) {
|
||||||
|
setValue(min)
|
||||||
|
;(ref as React.RefObject<HTMLInputElement>).current!.value = String(min)
|
||||||
|
} else if (value > max) {
|
||||||
|
setValue(max)
|
||||||
|
;(ref as React.RefObject<HTMLInputElement>).current!.value = String(max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex">
|
||||||
|
<NumericFormat
|
||||||
|
value={value}
|
||||||
|
onValueChange={handleChange}
|
||||||
|
thousandSeparator={thousandSeparator}
|
||||||
|
decimalScale={decimalScale}
|
||||||
|
fixedDecimalScale={fixedDecimalScale}
|
||||||
|
allowNegative={min < 0}
|
||||||
|
valueIsNumericString
|
||||||
|
onBlur={handleBlur}
|
||||||
|
max={max}
|
||||||
|
min={min}
|
||||||
|
suffix={suffix}
|
||||||
|
prefix={prefix}
|
||||||
|
customInput={(props) => <Input {...props} className={cn('w-full', className)} />}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||||
|
getInputRef={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<div className="absolute top-0 right-0 bottom-0 flex flex-col">
|
||||||
|
<Button
|
||||||
|
aria-label="Increase value"
|
||||||
|
className="border-input h-1/2 rounded-l-none rounded-br-none border-b border-l px-2 focus-visible:relative"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleIncrement}
|
||||||
|
disabled={value === max}
|
||||||
|
>
|
||||||
|
<ChevronUp size={15} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label="Decrease value"
|
||||||
|
className="border-input h-1/2 rounded-l-none rounded-tr-none border-b border-l px-2 focus-visible:relative"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDecrement}
|
||||||
|
disabled={value === min}
|
||||||
|
>
|
||||||
|
<ChevronDown size={15} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
NumberInput.displayName = 'NumberInput'
|
||||||
|
|
||||||
|
export default NumberInput
|
23
lightrag_webui/src/components/ui/Progress.tsx
Normal file
23
lightrag_webui/src/components/ui/Progress.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as ProgressPrimitive from '@radix-ui/react-progress'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('bg-secondary relative h-4 w-full overflow-hidden rounded-full', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="bg-primary h-full w-full flex-1 transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
))
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export default Progress
|
44
lightrag_webui/src/components/ui/ScrollArea.tsx
Normal file
44
lightrag_webui/src/components/ui/ScrollArea.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative overflow-hidden', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
))
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
'flex touch-none transition-colors select-none',
|
||||||
|
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||||
|
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" />
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
))
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
151
lightrag_webui/src/components/ui/Select.tsx
Normal file
151
lightrag_webui/src/components/ui/Select.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||||
|
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn('flex cursor-default items-center justify-center py-1', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = 'popper', ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md',
|
||||||
|
position === 'popper' &&
|
||||||
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
'p-1',
|
||||||
|
position === 'popper' &&
|
||||||
|
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn('py-1.5 pr-2 pl-8 text-sm font-semibold', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn('bg-muted -mx-1 my-1 h-px', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton
|
||||||
|
}
|
96
lightrag_webui/src/components/ui/Table.tsx
Normal file
96
lightrag_webui/src/components/ui/Table.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Table.displayName = 'Table'
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = 'TableHeader'
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
||||||
|
))
|
||||||
|
TableBody.displayName = 'TableBody'
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = 'TableFooter'
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
TableRow.displayName = 'TableRow'
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = 'TableHead'
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = 'TableCell'
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
|
||||||
|
))
|
||||||
|
TableCaption.displayName = 'TableCaption'
|
||||||
|
|
||||||
|
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
53
lightrag_webui/src/components/ui/Tabs.tsx
Normal file
53
lightrag_webui/src/components/ui/Tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-sm px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
@@ -8,19 +8,31 @@ const Tooltip = TooltipPrimitive.Root
|
|||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const processTooltipContent = (content: string) => {
|
||||||
|
if (typeof content !== 'string') return content
|
||||||
|
return content.split('\\n').map((line, i) => (
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
{line}
|
||||||
|
{i < content.split('\\n').length - 1 && <br />}
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ComponentRef<typeof TooltipPrimitive.Content>,
|
React.ComponentRef<typeof TooltipPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, children, ...props }, ref) => (
|
||||||
<TooltipPrimitive.Content
|
<TooltipPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
|
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1 max-w-sm overflow-hidden rounded-md border px-3 py-2 text-sm shadow-md',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{typeof children === 'string' ? processTooltipContent(children) : children}
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
))
|
))
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
5
lightrag_webui/src/features/ApiSite.tsx
Normal file
5
lightrag_webui/src/features/ApiSite.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { backendBaseUrl } from '@/lib/constants'
|
||||||
|
|
||||||
|
export default function ApiSite() {
|
||||||
|
return <iframe src={backendBaseUrl + '/docs'} className="size-full" />
|
||||||
|
}
|
166
lightrag_webui/src/features/DocumentManager.tsx
Normal file
166
lightrag_webui/src/features/DocumentManager.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@/components/ui/Table'
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
|
||||||
|
import EmptyCard from '@/components/ui/EmptyCard'
|
||||||
|
import Text from '@/components/ui/Text'
|
||||||
|
import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
|
||||||
|
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
|
||||||
|
|
||||||
|
import { getDocuments, scanNewDocuments, DocsStatusesResponse } from '@/api/lightrag'
|
||||||
|
import { errorMessage } from '@/lib/utils'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { useBackendState } from '@/stores/state'
|
||||||
|
|
||||||
|
import { RefreshCwIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function DocumentManager() {
|
||||||
|
const health = useBackendState.use.health()
|
||||||
|
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||||
|
|
||||||
|
const fetchDocuments = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const docs = await getDocuments()
|
||||||
|
if (docs && docs.statuses) {
|
||||||
|
setDocs(docs)
|
||||||
|
// console.log(docs)
|
||||||
|
} else {
|
||||||
|
setDocs(null)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to load documents\n' + errorMessage(err))
|
||||||
|
}
|
||||||
|
}, [setDocs])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDocuments()
|
||||||
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const scanDocuments = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const { status } = await scanNewDocuments()
|
||||||
|
toast.message(status)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to load documents\n' + errorMessage(err))
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
if (!health) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fetchDocuments()
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('Failed to get scan progress\n' + errorMessage(err))
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [health, fetchDocuments])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="!size-full !rounded-none !border-none">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Document Management</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={scanDocuments}
|
||||||
|
side="bottom"
|
||||||
|
tooltip="Scan documents"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<RefreshCwIcon /> Scan
|
||||||
|
</Button>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<ClearDocumentsDialog />
|
||||||
|
<UploadDocumentsDialog />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Uploaded documents</CardTitle>
|
||||||
|
<CardDescription>view the uploaded documents here</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{!docs && (
|
||||||
|
<EmptyCard
|
||||||
|
title="No documents uploaded"
|
||||||
|
description="upload documents to see them here"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{docs && (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>ID</TableHead>
|
||||||
|
<TableHead>Summary</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Length</TableHead>
|
||||||
|
<TableHead>Chunks</TableHead>
|
||||||
|
<TableHead>Created</TableHead>
|
||||||
|
<TableHead>Updated</TableHead>
|
||||||
|
<TableHead>Metadata</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody className="text-sm">
|
||||||
|
{Object.entries(docs.statuses).map(([status, documents]) =>
|
||||||
|
documents.map((doc) => (
|
||||||
|
<TableRow key={doc.id}>
|
||||||
|
<TableCell className="truncate font-mono">{doc.id}</TableCell>
|
||||||
|
<TableCell className="max-w-xs min-w-24 truncate">
|
||||||
|
<Text
|
||||||
|
text={doc.content_summary}
|
||||||
|
tooltip={doc.content_summary}
|
||||||
|
tooltipClassName="max-w-none overflow-visible block"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{status === 'processed' && (
|
||||||
|
<span className="text-green-600">Completed</span>
|
||||||
|
)}
|
||||||
|
{status === 'processing' && (
|
||||||
|
<span className="text-blue-600">Processing</span>
|
||||||
|
)}
|
||||||
|
{status === 'pending' && <span className="text-yellow-600">Pending</span>}
|
||||||
|
{status === 'failed' && <span className="text-red-600">Failed</span>}
|
||||||
|
{doc.error && (
|
||||||
|
<span className="ml-2 text-red-500" title={doc.error}>
|
||||||
|
⚠️
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{doc.content_length ?? '-'}</TableCell>
|
||||||
|
<TableCell>{doc.chunks_count ?? '-'}</TableCell>
|
||||||
|
<TableCell className="truncate">
|
||||||
|
{new Date(doc.created_at).toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="truncate">
|
||||||
|
{new Date(doc.updated_at).toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="max-w-xs truncate">
|
||||||
|
{doc.metadata ? JSON.stringify(doc.metadata) : '-'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
@@ -7,16 +7,16 @@ import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/ren
|
|||||||
import { NodeBorderProgram } from '@sigma/node-border'
|
import { NodeBorderProgram } from '@sigma/node-border'
|
||||||
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
|
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
|
||||||
|
|
||||||
import FocusOnNode from '@/components/FocusOnNode'
|
import FocusOnNode from '@/components/graph/FocusOnNode'
|
||||||
import LayoutsControl from '@/components/LayoutsControl'
|
import LayoutsControl from '@/components/graph/LayoutsControl'
|
||||||
import GraphControl from '@/components/GraphControl'
|
import GraphControl from '@/components/graph/GraphControl'
|
||||||
import ThemeToggle from '@/components/ThemeToggle'
|
// import ThemeToggle from '@/components/ThemeToggle'
|
||||||
import ZoomControl from '@/components/ZoomControl'
|
import ZoomControl from '@/components/graph/ZoomControl'
|
||||||
import FullScreenControl from '@/components/FullScreenControl'
|
import FullScreenControl from '@/components/graph/FullScreenControl'
|
||||||
import Settings from '@/components/Settings'
|
import Settings from '@/components/graph/Settings'
|
||||||
import GraphSearch from '@/components/GraphSearch'
|
import GraphSearch from '@/components/graph/GraphSearch'
|
||||||
import GraphLabels from '@/components/GraphLabels'
|
import GraphLabels from '@/components/graph/GraphLabels'
|
||||||
import PropertiesView from '@/components/PropertiesView'
|
import PropertiesView from '@/components/graph/PropertiesView'
|
||||||
|
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useGraphStore } from '@/stores/graph'
|
import { useGraphStore } from '@/stores/graph'
|
||||||
@@ -166,7 +166,7 @@ const GraphViewer = () => {
|
|||||||
<ZoomControl />
|
<ZoomControl />
|
||||||
<LayoutsControl />
|
<LayoutsControl />
|
||||||
<FullScreenControl />
|
<FullScreenControl />
|
||||||
<ThemeToggle />
|
{/* <ThemeToggle /> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPropertyPanel && (
|
{showPropertyPanel && (
|
161
lightrag_webui/src/features/RetrievalTesting.tsx
Normal file
161
lightrag_webui/src/features/RetrievalTesting.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import Input from '@/components/ui/Input'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { queryText, queryTextStream, Message } from '@/api/lightrag'
|
||||||
|
import { errorMessage } from '@/lib/utils'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useDebounce } from '@/hooks/useDebounce'
|
||||||
|
import QuerySettings from '@/components/retrieval/QuerySettings'
|
||||||
|
|
||||||
|
import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function RetrievalTesting() {
|
||||||
|
const [messages, setMessages] = useState<Message[]>(
|
||||||
|
() => useSettingsStore.getState().retrievalHistory || []
|
||||||
|
)
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const scrollToBottom = useCallback(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!inputValue.trim() || isLoading) return
|
||||||
|
|
||||||
|
// Create messages
|
||||||
|
const userMessage: Message = {
|
||||||
|
content: inputValue,
|
||||||
|
role: 'user'
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
content: '',
|
||||||
|
role: 'assistant'
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevMessages = [...messages]
|
||||||
|
|
||||||
|
// Add messages to chatbox
|
||||||
|
setMessages([...prevMessages, userMessage, assistantMessage])
|
||||||
|
|
||||||
|
// Clear input and set loading
|
||||||
|
setInputValue('')
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
// Create a function to update the assistant's message
|
||||||
|
const updateAssistantMessage = (chunk: string) => {
|
||||||
|
assistantMessage.content += chunk
|
||||||
|
setMessages((prev) => {
|
||||||
|
const newMessages = [...prev]
|
||||||
|
const lastMessage = newMessages[newMessages.length - 1]
|
||||||
|
if (lastMessage.role === 'assistant') {
|
||||||
|
lastMessage.content = assistantMessage.content
|
||||||
|
}
|
||||||
|
return newMessages
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare query parameters
|
||||||
|
const state = useSettingsStore.getState()
|
||||||
|
const queryParams = {
|
||||||
|
...state.querySettings,
|
||||||
|
query: userMessage.content,
|
||||||
|
conversation_history: prevMessages
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Run query
|
||||||
|
if (state.querySettings.stream) {
|
||||||
|
await queryTextStream(queryParams, updateAssistantMessage)
|
||||||
|
} else {
|
||||||
|
const response = await queryText(queryParams)
|
||||||
|
updateAssistantMessage(response.response)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Handle error
|
||||||
|
updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`)
|
||||||
|
} finally {
|
||||||
|
// Clear loading and add messages to state
|
||||||
|
setIsLoading(false)
|
||||||
|
useSettingsStore
|
||||||
|
.getState()
|
||||||
|
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[inputValue, isLoading, messages, setMessages]
|
||||||
|
)
|
||||||
|
|
||||||
|
const debouncedMessages = useDebounce(messages, 100)
|
||||||
|
useEffect(() => scrollToBottom(), [debouncedMessages, scrollToBottom])
|
||||||
|
|
||||||
|
const clearMessages = useCallback(() => {
|
||||||
|
setMessages([])
|
||||||
|
useSettingsStore.getState().setRetrievalHistory([])
|
||||||
|
}, [setMessages])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full gap-2 px-2 pb-12">
|
||||||
|
<div className="flex grow flex-col gap-4">
|
||||||
|
<div className="relative grow">
|
||||||
|
<div className="bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2">
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground flex h-full items-center justify-center text-lg">
|
||||||
|
Start a retrieval by typing your query below
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map((message, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] rounded-lg px-4 py-2 ${
|
||||||
|
message.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<pre className="break-words whitespace-pre-wrap">{message.content}</pre>
|
||||||
|
{message.content.length === 0 && (
|
||||||
|
<LoaderIcon className="animate-spin duration-2000" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} className="pb-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearMessages}
|
||||||
|
disabled={isLoading}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<EraserIcon />
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
className="flex-1"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
placeholder="Type your query..."
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<Button type="submit" variant="default" disabled={isLoading} size="sm">
|
||||||
|
<SendIcon />
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<QuerySettings />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
75
lightrag_webui/src/features/SiteHeader.tsx
Normal file
75
lightrag_webui/src/features/SiteHeader.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import { SiteInfo } from '@/lib/constants'
|
||||||
|
import ThemeToggle from '@/components/ThemeToggle'
|
||||||
|
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import { ZapIcon, GithubIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
interface NavigationTabProps {
|
||||||
|
value: string
|
||||||
|
currentTab: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
|
||||||
|
return (
|
||||||
|
<TabsTrigger
|
||||||
|
value={value}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer px-2 py-1 transition-all',
|
||||||
|
currentTab === value ? '!bg-emerald-400 !text-zinc-50' : 'hover:bg-background/60'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TabsTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsNavigation() {
|
||||||
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-8 self-center">
|
||||||
|
<TabsList className="h-full gap-2">
|
||||||
|
<NavigationTab value="documents" currentTab={currentTab}>
|
||||||
|
Documents
|
||||||
|
</NavigationTab>
|
||||||
|
<NavigationTab value="knowledge-graph" currentTab={currentTab}>
|
||||||
|
Knowledge Graph
|
||||||
|
</NavigationTab>
|
||||||
|
<NavigationTab value="retrieval" currentTab={currentTab}>
|
||||||
|
Retrieval
|
||||||
|
</NavigationTab>
|
||||||
|
<NavigationTab value="api" currentTab={currentTab}>
|
||||||
|
API
|
||||||
|
</NavigationTab>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SiteHeader() {
|
||||||
|
return (
|
||||||
|
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
|
||||||
|
<a href="/" className="mr-6 flex items-center gap-2">
|
||||||
|
<ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
|
||||||
|
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div className="flex h-10 flex-1 justify-center">
|
||||||
|
<TabsNavigation />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex items-center">
|
||||||
|
<Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
|
||||||
|
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||||
|
<GithubIcon className="size-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<ThemeToggle />
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@plugin 'tailwindcss-animate';
|
@plugin 'tailwindcss-animate';
|
||||||
|
@plugin 'tailwind-scrollbar';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@@ -142,3 +143,27 @@
|
|||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: hsl(0 0% 80%);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: hsl(0 0% 95%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: hsl(0 0% 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: hsl(0 0% 0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -23,3 +23,18 @@ export const maxNodeSize = 20
|
|||||||
export const healthCheckInterval = 15 // seconds
|
export const healthCheckInterval = 15 // seconds
|
||||||
|
|
||||||
export const defaultQueryLabel = '*'
|
export const defaultQueryLabel = '*'
|
||||||
|
|
||||||
|
// reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types
|
||||||
|
export const supportedFileTypes = {
|
||||||
|
'text/plain': ['.txt', '.md'],
|
||||||
|
'application/pdf': ['.pdf'],
|
||||||
|
'application/msword': ['.doc'],
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SiteInfo = {
|
||||||
|
name: 'LightRAG',
|
||||||
|
home: '/',
|
||||||
|
github: 'https://github.com/HKUDS/LightRAG'
|
||||||
|
}
|
||||||
|
@@ -2,8 +2,10 @@ import { create } from 'zustand'
|
|||||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||||
import { createSelectors } from '@/lib/utils'
|
import { createSelectors } from '@/lib/utils'
|
||||||
import { defaultQueryLabel } from '@/lib/constants'
|
import { defaultQueryLabel } from '@/lib/constants'
|
||||||
|
import { Message, QueryRequest } from '@/api/lightrag'
|
||||||
|
|
||||||
type Theme = 'dark' | 'light' | 'system'
|
type Theme = 'dark' | 'light' | 'system'
|
||||||
|
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
@@ -27,6 +29,15 @@ interface SettingsState {
|
|||||||
|
|
||||||
apiKey: string | null
|
apiKey: string | null
|
||||||
setApiKey: (key: string | null) => void
|
setApiKey: (key: string | null) => void
|
||||||
|
|
||||||
|
currentTab: Tab
|
||||||
|
setCurrentTab: (tab: Tab) => void
|
||||||
|
|
||||||
|
retrievalHistory: Message[]
|
||||||
|
setRetrievalHistory: (history: Message[]) => void
|
||||||
|
|
||||||
|
querySettings: Omit<QueryRequest, 'query'>
|
||||||
|
updateQuerySettings: (settings: Partial<QueryRequest>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSettingsStoreBase = create<SettingsState>()(
|
const useSettingsStoreBase = create<SettingsState>()(
|
||||||
@@ -49,6 +60,25 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
|
|
||||||
apiKey: null,
|
apiKey: null,
|
||||||
|
|
||||||
|
currentTab: 'documents',
|
||||||
|
|
||||||
|
retrievalHistory: [],
|
||||||
|
|
||||||
|
querySettings: {
|
||||||
|
mode: 'global',
|
||||||
|
response_type: 'Multiple Paragraphs',
|
||||||
|
top_k: 10,
|
||||||
|
max_token_for_text_unit: 4000,
|
||||||
|
max_token_for_global_context: 4000,
|
||||||
|
max_token_for_local_context: 4000,
|
||||||
|
only_need_context: false,
|
||||||
|
only_need_prompt: false,
|
||||||
|
stream: true,
|
||||||
|
history_turns: 3,
|
||||||
|
hl_keywords: [],
|
||||||
|
ll_keywords: []
|
||||||
|
},
|
||||||
|
|
||||||
setTheme: (theme: Theme) => set({ theme }),
|
setTheme: (theme: Theme) => set({ theme }),
|
||||||
|
|
||||||
setQueryLabel: (queryLabel: string) =>
|
setQueryLabel: (queryLabel: string) =>
|
||||||
@@ -58,12 +88,21 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
|
|
||||||
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
||||||
|
|
||||||
setApiKey: (apiKey: string | null) => set({ apiKey })
|
setApiKey: (apiKey: string | null) => set({ apiKey }),
|
||||||
|
|
||||||
|
setCurrentTab: (tab: Tab) => set({ currentTab: tab }),
|
||||||
|
|
||||||
|
setRetrievalHistory: (history: Message[]) => set({ retrievalHistory: history }),
|
||||||
|
|
||||||
|
updateQuerySettings: (settings: Partial<QueryRequest>) =>
|
||||||
|
set((state) => ({
|
||||||
|
querySettings: { ...state.querySettings, ...settings }
|
||||||
|
}))
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 4,
|
version: 6,
|
||||||
migrate: (state: any, version: number) => {
|
migrate: (state: any, version: number) => {
|
||||||
if (version < 2) {
|
if (version < 2) {
|
||||||
state.showEdgeLabel = false
|
state.showEdgeLabel = false
|
||||||
@@ -78,6 +117,27 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
state.enableHealthCheck = true
|
state.enableHealthCheck = true
|
||||||
state.apiKey = null
|
state.apiKey = null
|
||||||
}
|
}
|
||||||
|
if (version < 5) {
|
||||||
|
state.currentTab = 'documents'
|
||||||
|
}
|
||||||
|
if (version < 6) {
|
||||||
|
state.querySettings = {
|
||||||
|
mode: 'global',
|
||||||
|
response_type: 'Multiple Paragraphs',
|
||||||
|
top_k: 10,
|
||||||
|
max_token_for_text_unit: 4000,
|
||||||
|
max_token_for_global_context: 4000,
|
||||||
|
max_token_for_local_context: 4000,
|
||||||
|
only_need_context: false,
|
||||||
|
only_need_prompt: false,
|
||||||
|
stream: true,
|
||||||
|
history_turns: 3,
|
||||||
|
hl_keywords: [],
|
||||||
|
ll_keywords: []
|
||||||
|
}
|
||||||
|
state.retrievalHistory = []
|
||||||
|
}
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user