Merge branch 'main' into postgres-improve-entities-relation-save-process
This commit is contained in:
16
Dockerfile
16
Dockerfile
@@ -1,13 +1,23 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM python:3.11-slim as builder
|
FROM python:3.11-slim AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Rust and required build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||||
|
&& . $HOME/.cargo/env
|
||||||
|
|
||||||
# Copy only requirements files first to leverage Docker cache
|
# Copy only requirements files first to leverage Docker cache
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
COPY lightrag/api/requirements.txt ./lightrag/api/
|
COPY lightrag/api/requirements.txt ./lightrag/api/
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
RUN pip install --user --no-cache-dir -r requirements.txt
|
RUN pip install --user --no-cache-dir -r requirements.txt
|
||||||
RUN pip install --user --no-cache-dir -r lightrag/api/requirements.txt
|
RUN pip install --user --no-cache-dir -r lightrag/api/requirements.txt
|
||||||
|
|
||||||
@@ -28,6 +38,10 @@ ENV PATH=/root/.local/bin:$PATH
|
|||||||
# Create necessary directories
|
# Create necessary directories
|
||||||
RUN mkdir -p /app/data/rag_storage /app/data/inputs
|
RUN mkdir -p /app/data/rag_storage /app/data/inputs
|
||||||
|
|
||||||
|
# Docker data directories
|
||||||
|
ENV WORKING_DIR=/app/data/rag_storage
|
||||||
|
ENV INPUT_DIR=/app/data/inputs
|
||||||
|
|
||||||
# Expose the default port
|
# Expose the default port
|
||||||
EXPOSE 9621
|
EXPOSE 9621
|
||||||
|
|
||||||
|
0
README-zh.md
Normal file
0
README-zh.md
Normal file
233
README.md
233
README.md
@@ -37,28 +37,30 @@ This repository hosts the code of LightRAG. The structure of this code is based
|
|||||||
</br>
|
</br>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
|
<summary style="font-size: 1.4em; font-weight: bold; cursor: pointer; display: list-item;">
|
||||||
🎉 News
|
🎉 News
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
|
- [X] [2025.02.05]🎯📢Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG) understanding extremely long-context videos.
|
||||||
- [x] [2025.02.05]🎯📢Our team has released [VideoRAG](https://github.com/HKUDS/VideoRAG) understanding extremely long-context videos.
|
- [X] [2025.01.13]🎯📢Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
|
||||||
- [x] [2025.01.13]🎯📢Our team has released [MiniRAG](https://github.com/HKUDS/MiniRAG) making RAG simpler with small models.
|
- [X] [2025.01.06]🎯📢You can now [use PostgreSQL for Storage](#using-postgresql-for-storage).
|
||||||
- [x] [2025.01.06]🎯📢You can now [use PostgreSQL for Storage](#using-postgresql-for-storage).
|
- [X] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
|
||||||
- [x] [2024.12.31]🎯📢LightRAG now supports [deletion by document ID](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
|
- [X] [2024.11.25]🎯📢LightRAG now supports seamless integration of [custom knowledge graphs](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg), empowering users to enhance the system with their own domain expertise.
|
||||||
- [x] [2024.11.25]🎯📢LightRAG now supports seamless integration of [custom knowledge graphs](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#insert-custom-kg), empowering users to enhance the system with their own domain expertise.
|
- [X] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
|
||||||
- [x] [2024.11.19]🎯📢A comprehensive guide to LightRAG is now available on [LearnOpenCV](https://learnopencv.com/lightrag). Many thanks to the blog author.
|
- [X] [2024.11.12]🎯📢LightRAG now supports [Oracle Database 23ai for all storage types (KV, vector, and graph)](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_oracle_demo.py).
|
||||||
- [x] [2024.11.12]🎯📢LightRAG now supports [Oracle Database 23ai for all storage types (KV, vector, and graph)](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_oracle_demo.py).
|
- [X] [2024.11.11]🎯📢LightRAG now supports [deleting entities by their names](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
|
||||||
- [x] [2024.11.11]🎯📢LightRAG now supports [deleting entities by their names](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#delete).
|
- [X] [2024.11.09]🎯📢Introducing the [LightRAG Gui](https://lightrag-gui.streamlit.app), which allows you to insert, query, visualize, and download LightRAG knowledge.
|
||||||
- [x] [2024.11.09]🎯📢Introducing the [LightRAG Gui](https://lightrag-gui.streamlit.app), which allows you to insert, query, visualize, and download LightRAG knowledge.
|
- [X] [2024.11.04]🎯📢You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage).
|
||||||
- [x] [2024.11.04]🎯📢You can now [use Neo4J for Storage](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#using-neo4j-for-storage).
|
- [X] [2024.10.29]🎯📢LightRAG now supports multiple file types, including PDF, DOC, PPT, and CSV via `textract`.
|
||||||
- [x] [2024.10.29]🎯📢LightRAG now supports multiple file types, including PDF, DOC, PPT, and CSV via `textract`.
|
- [X] [2024.10.20]🎯📢We've added a new feature to LightRAG: Graph Visualization.
|
||||||
- [x] [2024.10.20]🎯📢We've added a new feature to LightRAG: Graph Visualization.
|
- [X] [2024.10.18]🎯📢We've added a link to a [LightRAG Introduction Video](https://youtu.be/oageL-1I0GE). Thanks to the author!
|
||||||
- [x] [2024.10.18]🎯📢We've added a link to a [LightRAG Introduction Video](https://youtu.be/oageL-1I0GE). Thanks to the author!
|
- [X] [2024.10.17]🎯📢We have created a [Discord channel](https://discord.gg/yF2MmDJyGJ)! Welcome to join for sharing and discussions! 🎉🎉
|
||||||
- [x] [2024.10.17]🎯📢We have created a [Discord channel](https://discord.gg/yF2MmDJyGJ)! Welcome to join for sharing and discussions! 🎉🎉
|
- [X] [2024.10.16]🎯📢LightRAG now supports [Ollama models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
|
||||||
- [x] [2024.10.16]🎯📢LightRAG now supports [Ollama models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
|
- [X] [2024.10.15]🎯📢LightRAG now supports [Hugging Face models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
|
||||||
- [x] [2024.10.15]🎯📢LightRAG now supports [Hugging Face models](https://github.com/HKUDS/LightRAG?tab=readme-ov-file#quick-start)!
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -82,16 +84,20 @@ This repository hosts the code of LightRAG. The structure of this code is based
|
|||||||
cd LightRAG
|
cd LightRAG
|
||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
* Install from PyPI
|
* Install from PyPI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install lightrag-hku
|
pip install lightrag-hku
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
* [Video demo](https://www.youtube.com/watch?v=g21royNJ4fw) of running LightRAG locally.
|
* [Video demo](https://www.youtube.com/watch?v=g21royNJ4fw) of running LightRAG locally.
|
||||||
* All the code can be found in the `examples`.
|
* All the code can be found in the `examples`.
|
||||||
* Set OpenAI API key in environment if using OpenAI models: `export OPENAI_API_KEY="sk-...".`
|
* Set OpenAI API key in environment if using OpenAI models: `export OPENAI_API_KEY="sk-...".`
|
||||||
* Download the demo text "A Christmas Carol by Charles Dickens":
|
* Download the demo text "A Christmas Carol by Charles Dickens":
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl https://raw.githubusercontent.com/gusye1234/nano-graphrag/main/tests/mock_data.txt > ./book.txt
|
curl https://raw.githubusercontent.com/gusye1234/nano-graphrag/main/tests/mock_data.txt > ./book.txt
|
||||||
```
|
```
|
||||||
@@ -187,6 +193,7 @@ class QueryParam:
|
|||||||
<summary> <b>Using Open AI-like APIs</b> </summary>
|
<summary> <b>Using Open AI-like APIs</b> </summary>
|
||||||
|
|
||||||
* LightRAG also supports Open AI-like chat/embeddings APIs:
|
* LightRAG also supports Open AI-like chat/embeddings APIs:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def llm_model_func(
|
async def llm_model_func(
|
||||||
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
|
prompt, system_prompt=None, history_messages=[], keyword_extraction=False, **kwargs
|
||||||
@@ -225,6 +232,7 @@ async def initialize_rag():
|
|||||||
|
|
||||||
return rag
|
return rag
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -252,12 +260,14 @@ rag = LightRAG(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> <b>Using Ollama Models</b> </summary>
|
<summary> <b>Using Ollama Models</b> </summary>
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
If you want to use Ollama models, you need to pull model you plan to use and embedding model, for example `nomic-embed-text`.
|
If you want to use Ollama models, you need to pull model you plan to use and embedding model, for example `nomic-embed-text`.
|
||||||
|
|
||||||
Then you only need to set LightRAG as follows:
|
Then you only need to set LightRAG as follows:
|
||||||
@@ -281,31 +291,37 @@ rag = LightRAG(
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Increasing context size
|
### Increasing context size
|
||||||
|
|
||||||
In order for LightRAG to work context should be at least 32k tokens. By default Ollama models have context size of 8k. You can achieve this using one of two ways:
|
In order for LightRAG to work context should be at least 32k tokens. By default Ollama models have context size of 8k. You can achieve this using one of two ways:
|
||||||
|
|
||||||
#### Increasing the `num_ctx` parameter in Modelfile.
|
#### Increasing the `num_ctx` parameter in Modelfile.
|
||||||
|
|
||||||
1. Pull the model:
|
1. Pull the model:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ollama pull qwen2
|
ollama pull qwen2
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Display the model file:
|
2. Display the model file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ollama show --modelfile qwen2 > Modelfile
|
ollama show --modelfile qwen2 > Modelfile
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Edit the Modelfile by adding the following line:
|
3. Edit the Modelfile by adding the following line:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PARAMETER num_ctx 32768
|
PARAMETER num_ctx 32768
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create the modified model:
|
4. Create the modified model:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ollama create -f Modelfile qwen2m
|
ollama create -f Modelfile qwen2m
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Setup `num_ctx` via Ollama API.
|
#### Setup `num_ctx` via Ollama API.
|
||||||
|
|
||||||
Tiy can use `llm_model_kwargs` param to configure ollama:
|
Tiy can use `llm_model_kwargs` param to configure ollama:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -325,6 +341,7 @@ rag = LightRAG(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Low RAM GPUs
|
#### Low RAM GPUs
|
||||||
|
|
||||||
In order to run this experiment on low RAM GPU you should select small model and tune context window (increasing context increase memory consumption). For example, running this ollama example on repurposed mining GPU with 6Gb of RAM required to set context size to 26k while using `gemma2:2b`. It was able to find 197 entities and 19 relations on `book.txt`.
|
In order to run this experiment on low RAM GPU you should select small model and tune context window (increasing context increase memory consumption). For example, running this ollama example on repurposed mining GPU with 6Gb of RAM required to set context size to 26k while using `gemma2:2b`. It was able to find 197 entities and 19 relations on `book.txt`.
|
||||||
@@ -402,6 +419,7 @@ if __name__ == "__main__":
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### For detailed documentation and examples, see:
|
#### For detailed documentation and examples, see:
|
||||||
|
|
||||||
- [LlamaIndex Documentation](lightrag/llm/Readme.md)
|
- [LlamaIndex Documentation](lightrag/llm/Readme.md)
|
||||||
- [Direct OpenAI Example](examples/lightrag_llamaindex_direct_demo.py)
|
- [Direct OpenAI Example](examples/lightrag_llamaindex_direct_demo.py)
|
||||||
- [LiteLLM Proxy Example](examples/lightrag_llamaindex_litellm_demo.py)
|
- [LiteLLM Proxy Example](examples/lightrag_llamaindex_litellm_demo.py)
|
||||||
@@ -483,13 +501,16 @@ print(response_custom)
|
|||||||
We've introduced a new function `query_with_separate_keyword_extraction` to enhance the keyword extraction capabilities. This function separates the keyword extraction process from the user's prompt, focusing solely on the query to improve the relevance of extracted keywords.
|
We've introduced a new function `query_with_separate_keyword_extraction` to enhance the keyword extraction capabilities. This function separates the keyword extraction process from the user's prompt, focusing solely on the query to improve the relevance of extracted keywords.
|
||||||
|
|
||||||
##### How It Works?
|
##### How It Works?
|
||||||
|
|
||||||
The function operates by dividing the input into two parts:
|
The function operates by dividing the input into two parts:
|
||||||
|
|
||||||
- `User Query`
|
- `User Query`
|
||||||
- `Prompt`
|
- `Prompt`
|
||||||
|
|
||||||
It then performs keyword extraction exclusively on the `user query`. This separation ensures that the extraction process is focused and relevant, unaffected by any additional language in the `prompt`. It also allows the `prompt` to serve purely for response formatting, maintaining the intent and clarity of the user's original question.
|
It then performs keyword extraction exclusively on the `user query`. This separation ensures that the extraction process is focused and relevant, unaffected by any additional language in the `prompt`. It also allows the `prompt` to serve purely for response formatting, maintaining the intent and clarity of the user's original question.
|
||||||
|
|
||||||
##### Usage Example
|
##### Usage Example
|
||||||
|
|
||||||
This `example` shows how to tailor the function for educational content, focusing on detailed explanations for older students.
|
This `example` shows how to tailor the function for educational content, focusing on detailed explanations for older students.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -563,6 +584,7 @@ custom_kg = {
|
|||||||
|
|
||||||
rag.insert_custom_kg(custom_kg)
|
rag.insert_custom_kg(custom_kg)
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Insert
|
## Insert
|
||||||
@@ -593,6 +615,7 @@ rag.insert(["TEXT1", "TEXT2", "TEXT3", ...]) # Documents will be processed in b
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `insert_batch_size` parameter in `addon_params` controls how many documents are processed in each batch during insertion. This is useful for:
|
The `insert_batch_size` parameter in `addon_params` controls how many documents are processed in each batch during insertion. This is useful for:
|
||||||
|
|
||||||
- Managing memory usage with large document collections
|
- Managing memory usage with large document collections
|
||||||
- Optimizing processing speed
|
- Optimizing processing speed
|
||||||
- Providing better progress tracking
|
- Providing better progress tracking
|
||||||
@@ -647,6 +670,7 @@ text_content = textract.process(file_path)
|
|||||||
|
|
||||||
rag.insert(text_content.decode('utf-8'))
|
rag.insert(text_content.decode('utf-8'))
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
@@ -685,6 +709,7 @@ async def initialize_rag():
|
|||||||
|
|
||||||
return rag
|
return rag
|
||||||
```
|
```
|
||||||
|
|
||||||
see test_neo4j.py for a working example.
|
see test_neo4j.py for a working example.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -693,6 +718,7 @@ see test_neo4j.py for a working example.
|
|||||||
<summary> <b>Using PostgreSQL for Storage</b> </summary>
|
<summary> <b>Using PostgreSQL for Storage</b> </summary>
|
||||||
|
|
||||||
For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE).
|
For production level scenarios you will most likely want to leverage an enterprise solution. PostgreSQL can provide a one-stop solution for you as KV store, VectorDB (pgvector) and GraphDB (apache AGE).
|
||||||
|
|
||||||
* PostgreSQL is lightweight,the whole binary distribution including all necessary plugins can be zipped to 40MB: Ref to [Windows Release](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0) as it is easy to install for Linux/Mac.
|
* PostgreSQL is lightweight,the whole binary distribution including all necessary plugins can be zipped to 40MB: Ref to [Windows Release](https://github.com/ShanGor/apache-age-windows/releases/tag/PG17%2Fv1.5.0-rc0) as it is easy to install for Linux/Mac.
|
||||||
* If you prefer docker, please start with this image if you are a beginner to avoid hiccups (DO read the overview): https://hub.docker.com/r/shangor/postgres-for-rag
|
* If you prefer docker, please start with this image if you are a beginner to avoid hiccups (DO read the overview): https://hub.docker.com/r/shangor/postgres-for-rag
|
||||||
* How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
|
* How to start? Ref to: [examples/lightrag_zhipu_postgres_demo.py](https://github.com/HKUDS/LightRAG/blob/main/examples/lightrag_zhipu_postgres_demo.py)
|
||||||
@@ -735,6 +761,7 @@ For production level scenarios you will most likely want to leverage an enterpri
|
|||||||
> It is a known issue of the release version: https://github.com/apache/age/pull/1721
|
> It is a known issue of the release version: https://github.com/apache/age/pull/1721
|
||||||
>
|
>
|
||||||
> You can Compile the AGE from source code and fix it.
|
> You can Compile the AGE from source code and fix it.
|
||||||
|
>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -742,9 +769,11 @@ For production level scenarios you will most likely want to leverage an enterpri
|
|||||||
<summary> <b>Using Faiss for Storage</b> </summary>
|
<summary> <b>Using Faiss for Storage</b> </summary>
|
||||||
|
|
||||||
- Install the required dependencies:
|
- Install the required dependencies:
|
||||||
|
|
||||||
```
|
```
|
||||||
pip install faiss-cpu
|
pip install faiss-cpu
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install `faiss-gpu` if you have GPU support.
|
You can also install `faiss-gpu` if you have GPU support.
|
||||||
|
|
||||||
- Here we are using `sentence-transformers` but you can also use `OpenAIEmbedding` model with `3072` dimensions.
|
- Here we are using `sentence-transformers` but you can also use `OpenAIEmbedding` model with `3072` dimensions.
|
||||||
@@ -810,6 +839,7 @@ relation = rag.create_relation("Google", "Gmail", {
|
|||||||
"weight": 2.0
|
"weight": 2.0
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -835,6 +865,7 @@ updated_relation = rag.edit_relation("Google", "Google Mail", {
|
|||||||
"weight": 3.0
|
"weight": 3.0
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
All operations are available in both synchronous and asynchronous versions. The asynchronous versions have the prefix "a" (e.g., `acreate_entity`, `aedit_relation`).
|
All operations are available in both synchronous and asynchronous versions. The asynchronous versions have the prefix "a" (e.g., `acreate_entity`, `aedit_relation`).
|
||||||
@@ -851,6 +882,55 @@ All operations are available in both synchronous and asynchronous versions. The
|
|||||||
|
|
||||||
These operations maintain data consistency across both the graph database and vector database components, ensuring your knowledge graph remains coherent.
|
These operations maintain data consistency across both the graph database and vector database components, ensuring your knowledge graph remains coherent.
|
||||||
|
|
||||||
|
## Data Export Functions
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
LightRAG allows you to export your knowledge graph data in various formats for analysis, sharing, and backup purposes. The system supports exporting entities, relations, and relationship data.
|
||||||
|
|
||||||
|
## Export Functions
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Basic CSV export (default format)
|
||||||
|
rag.export_data("knowledge_graph.csv")
|
||||||
|
|
||||||
|
# Specify any format
|
||||||
|
rag.export_data("output.xlsx", file_format="excel")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Different File Formats supported
|
||||||
|
|
||||||
|
```python
|
||||||
|
#Export data in CSV format
|
||||||
|
rag.export_data("graph_data.csv", file_format="csv")
|
||||||
|
|
||||||
|
# Export data in Excel sheet
|
||||||
|
rag.export_data("graph_data.xlsx", file_format="excel")
|
||||||
|
|
||||||
|
# Export data in markdown format
|
||||||
|
rag.export_data("graph_data.md", file_format="md")
|
||||||
|
|
||||||
|
# Export data in Text
|
||||||
|
rag.export_data("graph_data.txt", file_format="txt")
|
||||||
|
```
|
||||||
|
## Additional Options
|
||||||
|
|
||||||
|
Include vector embeddings in the export (optional):
|
||||||
|
|
||||||
|
```python
|
||||||
|
rag.export_data("complete_data.csv", include_vector_data=True)
|
||||||
|
```
|
||||||
|
## Data Included in Export
|
||||||
|
|
||||||
|
All exports include:
|
||||||
|
|
||||||
|
* Entity information (names, IDs, metadata)
|
||||||
|
* Relation data (connections between entities)
|
||||||
|
* Relationship information from vector database
|
||||||
|
|
||||||
|
|
||||||
## Entity Merging
|
## Entity Merging
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -913,6 +993,7 @@ rag.merge_entities(
|
|||||||
```
|
```
|
||||||
|
|
||||||
When merging entities:
|
When merging entities:
|
||||||
|
|
||||||
* All relationships from source entities are redirected to the target entity
|
* All relationships from source entities are redirected to the target entity
|
||||||
* Duplicate relationships are intelligently merged
|
* Duplicate relationships are intelligently merged
|
||||||
* Self-relationships (loops) are prevented
|
* Self-relationships (loops) are prevented
|
||||||
@@ -946,6 +1027,7 @@ rag.clear_cache(modes=["local"])
|
|||||||
```
|
```
|
||||||
|
|
||||||
Valid modes are:
|
Valid modes are:
|
||||||
|
|
||||||
- `"default"`: Extraction cache
|
- `"default"`: Extraction cache
|
||||||
- `"naive"`: Naive search cache
|
- `"naive"`: Naive search cache
|
||||||
- `"local"`: Local search cache
|
- `"local"`: Local search cache
|
||||||
@@ -960,33 +1042,33 @@ Valid modes are:
|
|||||||
<details>
|
<details>
|
||||||
<summary> Parameters </summary>
|
<summary> Parameters </summary>
|
||||||
|
|
||||||
| **Parameter** | **Type** | **Explanation** | **Default** |
|
| **Parameter** | **Type** | **Explanation** | **Default** |
|
||||||
|----------------------------------------------| --- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
|
| -------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||||
| **working\_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
|
| **working\_dir** | `str` | Directory where the cache will be stored | `lightrag_cache+timestamp` |
|
||||||
| **kv\_storage** | `str` | Storage type for documents and text chunks. Supported types: `JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
|
| **kv\_storage** | `str` | Storage type for documents and text chunks. Supported types:`JsonKVStorage`, `OracleKVStorage` | `JsonKVStorage` |
|
||||||
| **vector\_storage** | `str` | Storage type for embedding vectors. Supported types: `NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
|
| **vector\_storage** | `str` | Storage type for embedding vectors. Supported types:`NanoVectorDBStorage`, `OracleVectorDBStorage` | `NanoVectorDBStorage` |
|
||||||
| **graph\_storage** | `str` | Storage type for graph edges and nodes. Supported types: `NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
|
| **graph\_storage** | `str` | Storage type for graph edges and nodes. Supported types:`NetworkXStorage`, `Neo4JStorage`, `OracleGraphStorage` | `NetworkXStorage` |
|
||||||
| **chunk\_token\_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
|
| **chunk\_token\_size** | `int` | Maximum token size per chunk when splitting documents | `1200` |
|
||||||
| **chunk\_overlap\_token\_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
|
| **chunk\_overlap\_token\_size** | `int` | Overlap token size between two chunks when splitting documents | `100` |
|
||||||
| **tiktoken\_model\_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
|
| **tiktoken\_model\_name** | `str` | Model name for the Tiktoken encoder used to calculate token numbers | `gpt-4o-mini` |
|
||||||
| **entity\_extract\_max\_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
|
| **entity\_extract\_max\_gleaning** | `int` | Number of loops in the entity extraction process, appending history messages | `1` |
|
||||||
| **entity\_summary\_to\_max\_tokens** | `int` | Maximum token size for each entity summary | `500` |
|
| **entity\_summary\_to\_max\_tokens** | `int` | Maximum token size for each entity summary | `500` |
|
||||||
| **node\_embedding\_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
|
| **node\_embedding\_algorithm** | `str` | Algorithm for node embedding (currently not used) | `node2vec` |
|
||||||
| **node2vec\_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
|
| **node2vec\_params** | `dict` | Parameters for node embedding | `{"dimensions": 1536,"num_walks": 10,"walk_length": 40,"window_size": 2,"iterations": 3,"random_seed": 3,}` |
|
||||||
| **embedding\_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embed` |
|
| **embedding\_func** | `EmbeddingFunc` | Function to generate embedding vectors from text | `openai_embed` |
|
||||||
| **embedding\_batch\_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
|
| **embedding\_batch\_num** | `int` | Maximum batch size for embedding processes (multiple texts sent per batch) | `32` |
|
||||||
| **embedding\_func\_max\_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
|
| **embedding\_func\_max\_async** | `int` | Maximum number of concurrent asynchronous embedding processes | `16` |
|
||||||
| **llm\_model\_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
|
| **llm\_model\_func** | `callable` | Function for LLM generation | `gpt_4o_mini_complete` |
|
||||||
| **llm\_model\_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
|
| **llm\_model\_name** | `str` | LLM model name for generation | `meta-llama/Llama-3.2-1B-Instruct` |
|
||||||
| **llm\_model\_max\_token\_size** | `int` | Maximum token size for LLM generation (affects entity relation summaries) | `32768`(default value changed by env var MAX_TOKENS) |
|
| **llm\_model\_max\_token\_size** | `int` | Maximum token size for LLM generation (affects entity relation summaries) | `32768`(default value changed by env var MAX_TOKENS) |
|
||||||
| **llm\_model\_max\_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `16`(default value changed by env var MAX_ASYNC) |
|
| **llm\_model\_max\_async** | `int` | Maximum number of concurrent asynchronous LLM processes | `4`(default value changed by env var MAX_ASYNC) |
|
||||||
| **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
|
| **llm\_model\_kwargs** | `dict` | Additional parameters for LLM generation | |
|
||||||
| **vector\_db\_storage\_cls\_kwargs** | `dict` | Additional parameters for vector database, like setting the threshold for nodes and relations retrieval. | cosine_better_than_threshold: 0.2(default value changed by env var COSINE_THRESHOLD) |
|
| **vector\_db\_storage\_cls\_kwargs** | `dict` | Additional parameters for vector database, like setting the threshold for nodes and relations retrieval. | cosine_better_than_threshold: 0.2(default value changed by env var COSINE_THRESHOLD) |
|
||||||
| **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
|
| **enable\_llm\_cache** | `bool` | If `TRUE`, stores LLM results in cache; repeated prompts return cached responses | `TRUE` |
|
||||||
| **enable\_llm\_cache\_for\_entity\_extract** | `bool` | If `TRUE`, stores LLM results in cache for entity extraction; Good for beginners to debug your application | `TRUE` |
|
| **enable\_llm\_cache\_for\_entity\_extract** | `bool` | If `TRUE`, stores LLM results in cache for entity extraction; Good for beginners to debug your application | `TRUE` |
|
||||||
| **addon\_params** | `dict` | Additional parameters, e.g., `{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"], "insert_batch_size": 10}`: sets example limit, output language, and batch size for document processing | `example_number: all examples, language: English, insert_batch_size: 10` |
|
| **addon\_params** | `dict` | Additional parameters, e.g.,`{"example_number": 1, "language": "Simplified Chinese", "entity_types": ["organization", "person", "geo", "event"], "insert_batch_size": 10}`: sets example limit, output language, and batch size for document processing | `example_number: all examples, language: English, insert_batch_size: 10` |
|
||||||
| **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
|
| **convert\_response\_to\_json\_func** | `callable` | Not used | `convert_response_to_json` |
|
||||||
| **embedding\_cache\_config** | `dict` | Configuration for question-answer caching. Contains three parameters:<br>- `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers.<br>- `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM.<br>- `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default: `{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
|
| **embedding\_cache\_config** | `dict` | Configuration for question-answer caching. Contains three parameters:`<br>`- `enabled`: Boolean value to enable/disable cache lookup functionality. When enabled, the system will check cached responses before generating new answers.`<br>`- `similarity_threshold`: Float value (0-1), similarity threshold. When a new question's similarity with a cached question exceeds this threshold, the cached answer will be returned directly without calling the LLM.`<br>`- `use_llm_check`: Boolean value to enable/disable LLM similarity verification. When enabled, LLM will be used as a secondary check to verify the similarity between questions before returning cached answers. | Default:`{"enabled": False, "similarity_threshold": 0.95, "use_llm_check": False}` |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -996,12 +1078,15 @@ Valid modes are:
|
|||||||
<summary>Click to view error handling details</summary>
|
<summary>Click to view error handling details</summary>
|
||||||
|
|
||||||
The API includes comprehensive error handling:
|
The API includes comprehensive error handling:
|
||||||
|
|
||||||
- File not found errors (404)
|
- File not found errors (404)
|
||||||
- Processing errors (500)
|
- Processing errors (500)
|
||||||
- Supports multiple file encodings (UTF-8 and GBK)
|
- Supports multiple file encodings (UTF-8 and GBK)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
LightRag can be installed with API support to serve a Fast api interface to perform data upload and indexing/Rag operations/Rescan of the input folder etc..
|
LightRag can be installed with API support to serve a Fast api interface to perform data upload and indexing/Rag operations/Rescan of the input folder etc..
|
||||||
|
|
||||||
[LightRag API](lightrag/api/README.md)
|
[LightRag API](lightrag/api/README.md)
|
||||||
@@ -1035,7 +1120,6 @@ net.show('knowledge_graph.html')
|
|||||||
<details>
|
<details>
|
||||||
<summary> <b>Graph visualization with Neo4</b> </summary>
|
<summary> <b>Graph visualization with Neo4</b> </summary>
|
||||||
|
|
||||||
|
|
||||||
* The following code can be found in `examples/graph_visual_with_neo4j.py`
|
* The following code can be found in `examples/graph_visual_with_neo4j.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -1171,10 +1255,13 @@ LightRag can be installed with Tools support to add extra tools like the graphml
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Evaluation
|
## Evaluation
|
||||||
|
|
||||||
### Dataset
|
### Dataset
|
||||||
|
|
||||||
The dataset used in LightRAG can be downloaded from [TommyChien/UltraDomain](https://huggingface.co/datasets/TommyChien/UltraDomain).
|
The dataset used in LightRAG can be downloaded from [TommyChien/UltraDomain](https://huggingface.co/datasets/TommyChien/UltraDomain).
|
||||||
|
|
||||||
### Generate Query
|
### Generate Query
|
||||||
|
|
||||||
LightRAG uses the following prompt to generate high-level queries, with the corresponding code in `example/generate_query.py`.
|
LightRAG uses the following prompt to generate high-level queries, with the corresponding code in `example/generate_query.py`.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -1203,9 +1290,11 @@ Output the results in the following structure:
|
|||||||
- User 5: [user description]
|
- User 5: [user description]
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Batch Eval
|
### Batch Eval
|
||||||
|
|
||||||
To evaluate the performance of two RAG systems on high-level queries, LightRAG uses the following prompt, with the specific code available in `example/batch_eval.py`.
|
To evaluate the performance of two RAG systems on high-level queries, LightRAG uses the following prompt, with the specific code available in `example/batch_eval.py`.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -1253,37 +1342,40 @@ Output your evaluation in the following JSON format:
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Overall Performance Table
|
### Overall Performance Table
|
||||||
|
|
||||||
| | **Agriculture** | | **CS** | | **Legal** | | **Mix** | |
|
| | **Agriculture** | | **CS** | | **Legal** | | **Mix** | |
|
||||||
|----------------------|-------------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------|-----------------------|
|
| --------------------------- | --------------------- | ------------------ | ------------ | ------------------ | --------------- | ------------------ | --------------- | ------------------ |
|
||||||
| | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** |
|
| | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** | NaiveRAG | **LightRAG** |
|
||||||
| **Comprehensiveness** | 32.4% | **67.6%** | 38.4% | **61.6%** | 16.4% | **83.6%** | 38.8% | **61.2%** |
|
| **Comprehensiveness** | 32.4% | **67.6%** | 38.4% | **61.6%** | 16.4% | **83.6%** | 38.8% | **61.2%** |
|
||||||
| **Diversity** | 23.6% | **76.4%** | 38.0% | **62.0%** | 13.6% | **86.4%** | 32.4% | **67.6%** |
|
| **Diversity** | 23.6% | **76.4%** | 38.0% | **62.0%** | 13.6% | **86.4%** | 32.4% | **67.6%** |
|
||||||
| **Empowerment** | 32.4% | **67.6%** | 38.8% | **61.2%** | 16.4% | **83.6%** | 42.8% | **57.2%** |
|
| **Empowerment** | 32.4% | **67.6%** | 38.8% | **61.2%** | 16.4% | **83.6%** | 42.8% | **57.2%** |
|
||||||
| **Overall** | 32.4% | **67.6%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 40.0% | **60.0%** |
|
| **Overall** | 32.4% | **67.6%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 40.0% | **60.0%** |
|
||||||
| | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** |
|
| | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** | RQ-RAG | **LightRAG** |
|
||||||
| **Comprehensiveness** | 31.6% | **68.4%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 39.2% | **60.8%** |
|
| **Comprehensiveness** | 31.6% | **68.4%** | 38.8% | **61.2%** | 15.2% | **84.8%** | 39.2% | **60.8%** |
|
||||||
| **Diversity** | 29.2% | **70.8%** | 39.2% | **60.8%** | 11.6% | **88.4%** | 30.8% | **69.2%** |
|
| **Diversity** | 29.2% | **70.8%** | 39.2% | **60.8%** | 11.6% | **88.4%** | 30.8% | **69.2%** |
|
||||||
| **Empowerment** | 31.6% | **68.4%** | 36.4% | **63.6%** | 15.2% | **84.8%** | 42.4% | **57.6%** |
|
| **Empowerment** | 31.6% | **68.4%** | 36.4% | **63.6%** | 15.2% | **84.8%** | 42.4% | **57.6%** |
|
||||||
| **Overall** | 32.4% | **67.6%** | 38.0% | **62.0%** | 14.4% | **85.6%** | 40.0% | **60.0%** |
|
| **Overall** | 32.4% | **67.6%** | 38.0% | **62.0%** | 14.4% | **85.6%** | 40.0% | **60.0%** |
|
||||||
| | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** |
|
| | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** | HyDE | **LightRAG** |
|
||||||
| **Comprehensiveness** | 26.0% | **74.0%** | 41.6% | **58.4%** | 26.8% | **73.2%** | 40.4% | **59.6%** |
|
| **Comprehensiveness** | 26.0% | **74.0%** | 41.6% | **58.4%** | 26.8% | **73.2%** | 40.4% | **59.6%** |
|
||||||
| **Diversity** | 24.0% | **76.0%** | 38.8% | **61.2%** | 20.0% | **80.0%** | 32.4% | **67.6%** |
|
| **Diversity** | 24.0% | **76.0%** | 38.8% | **61.2%** | 20.0% | **80.0%** | 32.4% | **67.6%** |
|
||||||
| **Empowerment** | 25.2% | **74.8%** | 40.8% | **59.2%** | 26.0% | **74.0%** | 46.0% | **54.0%** |
|
| **Empowerment** | 25.2% | **74.8%** | 40.8% | **59.2%** | 26.0% | **74.0%** | 46.0% | **54.0%** |
|
||||||
| **Overall** | 24.8% | **75.2%** | 41.6% | **58.4%** | 26.4% | **73.6%** | 42.4% | **57.6%** |
|
| **Overall** | 24.8% | **75.2%** | 41.6% | **58.4%** | 26.4% | **73.6%** | 42.4% | **57.6%** |
|
||||||
| | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** |
|
| | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** | GraphRAG | **LightRAG** |
|
||||||
| **Comprehensiveness** | 45.6% | **54.4%** | 48.4% | **51.6%** | 48.4% | **51.6%** | **50.4%** | 49.6% |
|
| **Comprehensiveness** | 45.6% | **54.4%** | 48.4% | **51.6%** | 48.4% | **51.6%** | **50.4%** | 49.6% |
|
||||||
| **Diversity** | 22.8% | **77.2%** | 40.8% | **59.2%** | 26.4% | **73.6%** | 36.0% | **64.0%** |
|
| **Diversity** | 22.8% | **77.2%** | 40.8% | **59.2%** | 26.4% | **73.6%** | 36.0% | **64.0%** |
|
||||||
| **Empowerment** | 41.2% | **58.8%** | 45.2% | **54.8%** | 43.6% | **56.4%** | **50.8%** | 49.2% |
|
| **Empowerment** | 41.2% | **58.8%** | 45.2% | **54.8%** | 43.6% | **56.4%** | **50.8%** | 49.2% |
|
||||||
| **Overall** | 45.2% | **54.8%** | 48.0% | **52.0%** | 47.2% | **52.8%** | **50.4%** | 49.6% |
|
| **Overall** | 45.2% | **54.8%** | 48.0% | **52.0%** | 47.2% | **52.8%** | **50.4%** | 49.6% |
|
||||||
|
|
||||||
## Reproduce
|
## Reproduce
|
||||||
|
|
||||||
All the code can be found in the `./reproduce` directory.
|
All the code can be found in the `./reproduce` directory.
|
||||||
|
|
||||||
### Step-0 Extract Unique Contexts
|
### Step-0 Extract Unique Contexts
|
||||||
|
|
||||||
First, we need to extract unique contexts in the datasets.
|
First, we need to extract unique contexts in the datasets.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -1340,9 +1432,11 @@ def extract_unique_contexts(input_directory, output_directory):
|
|||||||
print("All files have been processed.")
|
print("All files have been processed.")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Step-1 Insert Contexts
|
### Step-1 Insert Contexts
|
||||||
|
|
||||||
For the extracted contexts, we insert them into the LightRAG system.
|
For the extracted contexts, we insert them into the LightRAG system.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -1366,6 +1460,7 @@ def insert_text(rag, file_path):
|
|||||||
if retries == max_retries:
|
if retries == max_retries:
|
||||||
print("Insertion failed after exceeding the maximum number of retries")
|
print("Insertion failed after exceeding the maximum number of retries")
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Step-2 Generate Queries
|
### Step-2 Generate Queries
|
||||||
@@ -1390,9 +1485,11 @@ def get_summary(context, tot_tokens=2000):
|
|||||||
|
|
||||||
return summary
|
return summary
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Step-3 Query
|
### Step-3 Query
|
||||||
|
|
||||||
For the queries generated in Step-2, we will extract them and query LightRAG.
|
For the queries generated in Step-2, we will extract them and query LightRAG.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -1409,6 +1506,7 @@ def extract_queries(file_path):
|
|||||||
|
|
||||||
return queries
|
return queries
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
@@ -1441,4 +1539,5 @@ archivePrefix={arXiv},
|
|||||||
primaryClass={cs.IR}
|
primaryClass={cs.IR}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Thank you for your interest in our work!**
|
**Thank you for your interest in our work!**
|
||||||
|
@@ -50,7 +50,8 @@
|
|||||||
# MAX_TOKEN_SUMMARY=500 # Max tokens for entity or relations summary
|
# MAX_TOKEN_SUMMARY=500 # Max tokens for entity or relations summary
|
||||||
# SUMMARY_LANGUAGE=English
|
# SUMMARY_LANGUAGE=English
|
||||||
# MAX_EMBED_TOKENS=8192
|
# MAX_EMBED_TOKENS=8192
|
||||||
# ENABLE_LLM_CACHE_FOR_EXTRACT=false # Enable LLM cache for entity extraction, defaults to false
|
# ENABLE_LLM_CACHE_FOR_EXTRACT=true # Enable LLM cache for entity extraction
|
||||||
|
# MAX_PARALLEL_INSERT=2 # Maximum number of parallel processing documents in pipeline
|
||||||
|
|
||||||
### LLM Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
|
### LLM Configuration (Use valid host. For local services installed with docker, you can use host.docker.internal)
|
||||||
LLM_BINDING=ollama
|
LLM_BINDING=ollama
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
from .lightrag import LightRAG as LightRAG, QueryParam as QueryParam
|
||||||
|
|
||||||
__version__ = "1.2.5"
|
__version__ = "1.2.6"
|
||||||
__author__ = "Zirui Guo"
|
__author__ = "Zirui Guo"
|
||||||
__url__ = "https://github.com/HKUDS/LightRAG"
|
__url__ = "https://github.com/HKUDS/LightRAG"
|
||||||
|
@@ -224,7 +224,7 @@ LightRAG supports binding to various LLM/Embedding backends:
|
|||||||
Use environment variables `LLM_BINDING` or CLI argument `--llm-binding` to select LLM backend type. Use environment variables `EMBEDDING_BINDING` or CLI argument `--embedding-binding` to select LLM backend type.
|
Use environment variables `LLM_BINDING` or CLI argument `--llm-binding` to select LLM backend type. Use environment variables `EMBEDDING_BINDING` or CLI argument `--embedding-binding` to select LLM backend type.
|
||||||
|
|
||||||
### Entity Extraction Configuration
|
### Entity Extraction Configuration
|
||||||
* ENABLE_LLM_CACHE_FOR_EXTRACT: Enable LLM cache for entity extraction (default: false)
|
* ENABLE_LLM_CACHE_FOR_EXTRACT: Enable LLM cache for entity extraction (default: true)
|
||||||
|
|
||||||
It's very common to set `ENABLE_LLM_CACHE_FOR_EXTRACT` to true for test environment to reduce the cost of LLM calls.
|
It's very common to set `ENABLE_LLM_CACHE_FOR_EXTRACT` to true for test environment to reduce the cost of LLM calls.
|
||||||
|
|
||||||
|
@@ -141,7 +141,7 @@ Start the LightRAG server using specified options:
|
|||||||
lightrag-server --port 9621 --key sk-somepassword --kv-storage PGKVStorage --graph-storage PGGraphStorage --vector-storage PGVectorStorage --doc-status-storage PGDocStatusStorage
|
lightrag-server --port 9621 --key sk-somepassword --kv-storage PGKVStorage --graph-storage PGGraphStorage --vector-storage PGVectorStorage --doc-status-storage PGDocStatusStorage
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `the-port-number` with your desired port number (default is 9621) and `your-secret-key` with a secure key.
|
Replace the `port` number with your desired port number (default is 9621) and `your-secret-key` with a secure key.
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
|
@@ -59,7 +59,7 @@ logconfig_dict = {
|
|||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"path_filter": {
|
"path_filter": {
|
||||||
"()": "lightrag.api.lightrag_server.LightragPathFilter",
|
"()": "lightrag.utils.LightragPathFilter",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
|
@@ -55,41 +55,6 @@ config = configparser.ConfigParser()
|
|||||||
config.read("config.ini")
|
config.read("config.ini")
|
||||||
|
|
||||||
|
|
||||||
class LightragPathFilter(logging.Filter):
|
|
||||||
"""Filter for lightrag logger to filter out frequent path access logs"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
# Define paths to be filtered
|
|
||||||
self.filtered_paths = ["/documents", "/health", "/webui/"]
|
|
||||||
|
|
||||||
def filter(self, record):
|
|
||||||
try:
|
|
||||||
# Check if record has the required attributes for an access log
|
|
||||||
if not hasattr(record, "args") or not isinstance(record.args, tuple):
|
|
||||||
return True
|
|
||||||
if len(record.args) < 5:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Extract method, path and status from the record args
|
|
||||||
method = record.args[1]
|
|
||||||
path = record.args[2]
|
|
||||||
status = record.args[4]
|
|
||||||
|
|
||||||
# Filter out successful GET requests to filtered paths
|
|
||||||
if (
|
|
||||||
method == "GET"
|
|
||||||
and (status == 200 or status == 304)
|
|
||||||
and path in self.filtered_paths
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
# In case of any error, let the message through
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(args):
|
def create_app(args):
|
||||||
# Setup logging
|
# Setup logging
|
||||||
logger.setLevel(args.log_level)
|
logger.setLevel(args.log_level)
|
||||||
@@ -177,6 +142,9 @@ def create_app(args):
|
|||||||
if api_key
|
if api_key
|
||||||
else "",
|
else "",
|
||||||
version=__api_version__,
|
version=__api_version__,
|
||||||
|
openapi_url="/openapi.json", # Explicitly set OpenAPI schema URL
|
||||||
|
docs_url="/docs", # Explicitly set docs URL
|
||||||
|
redoc_url="/redoc", # Explicitly set redoc URL
|
||||||
openapi_tags=[{"name": "api"}],
|
openapi_tags=[{"name": "api"}],
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
@@ -423,12 +391,24 @@ def create_app(args):
|
|||||||
"update_status": update_status,
|
"update_status": update_status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Custom StaticFiles class to prevent caching of HTML files
|
||||||
|
class NoCacheStaticFiles(StaticFiles):
|
||||||
|
async def get_response(self, path: str, scope):
|
||||||
|
response = await super().get_response(path, scope)
|
||||||
|
if path.endswith(".html"):
|
||||||
|
response.headers["Cache-Control"] = (
|
||||||
|
"no-cache, no-store, must-revalidate"
|
||||||
|
)
|
||||||
|
response.headers["Pragma"] = "no-cache"
|
||||||
|
response.headers["Expires"] = "0"
|
||||||
|
return response
|
||||||
|
|
||||||
# Webui mount webui/index.html
|
# Webui mount webui/index.html
|
||||||
static_dir = Path(__file__).parent / "webui"
|
static_dir = Path(__file__).parent / "webui"
|
||||||
static_dir.mkdir(exist_ok=True)
|
static_dir.mkdir(exist_ok=True)
|
||||||
app.mount(
|
app.mount(
|
||||||
"/webui",
|
"/webui",
|
||||||
StaticFiles(directory=static_dir, html=True, check_dir=True),
|
NoCacheStaticFiles(directory=static_dir, html=True, check_dir=True),
|
||||||
name="webui",
|
name="webui",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -516,7 +496,7 @@ def configure_logging():
|
|||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"path_filter": {
|
"path_filter": {
|
||||||
"()": "lightrag.api.lightrag_server.LightragPathFilter",
|
"()": "lightrag.utils.LightragPathFilter",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ ascii_colors
|
|||||||
asyncpg
|
asyncpg
|
||||||
distro
|
distro
|
||||||
fastapi
|
fastapi
|
||||||
|
graspologic>=3.4.1
|
||||||
httpcore
|
httpcore
|
||||||
httpx
|
httpx
|
||||||
jiter
|
jiter
|
||||||
|
@@ -364,7 +364,7 @@ def parse_args(is_uvicorn_mode: bool = False) -> argparse.Namespace:
|
|||||||
|
|
||||||
# Inject LLM cache configuration
|
# Inject LLM cache configuration
|
||||||
args.enable_llm_cache_for_extract = get_env_value(
|
args.enable_llm_cache_for_extract = get_env_value(
|
||||||
"ENABLE_LLM_CACHE_FOR_EXTRACT", False, bool
|
"ENABLE_LLM_CACHE_FOR_EXTRACT", True, bool
|
||||||
)
|
)
|
||||||
|
|
||||||
# Select Document loading tool (DOCLING, DEFAULT)
|
# Select Document loading tool (DOCLING, DEFAULT)
|
||||||
|
1
lightrag/api/webui/assets/index-BV5s8k-a.css
Normal file
1
lightrag/api/webui/assets/index-BV5s8k-a.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,11 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="./logo.png" />
|
<link rel="icon" type="image/svg+xml" href="./logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
<script type="module" crossorigin src="./assets/index-BlVvSIic.js"></script>
|
<script type="module" crossorigin src="./assets/index-DwcJE583.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-CH-3l4_Z.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BV5s8k-a.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -767,7 +767,7 @@ class PGDocStatusStorage(DocStatusStorage):
|
|||||||
result = await self.db.query(sql, params, True)
|
result = await self.db.query(sql, params, True)
|
||||||
docs_by_status = {
|
docs_by_status = {
|
||||||
element["id"]: DocProcessingStatus(
|
element["id"]: DocProcessingStatus(
|
||||||
content=result[0]["content"],
|
content=element["content"],
|
||||||
content_summary=element["content_summary"],
|
content_summary=element["content_summary"],
|
||||||
content_length=element["content_length"],
|
content_length=element["content_length"],
|
||||||
status=element["status"],
|
status=element["status"],
|
||||||
@@ -1572,7 +1572,7 @@ TABLES = {
|
|||||||
content_vector VECTOR,
|
content_vector VECTOR,
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time TIMESTAMP,
|
update_time TIMESTAMP,
|
||||||
chunk_ids VARCHAR(255)[] NULL,
|
chunk_id TEXT NULL,
|
||||||
CONSTRAINT LIGHTRAG_VDB_ENTITY_PK PRIMARY KEY (workspace, id)
|
CONSTRAINT LIGHTRAG_VDB_ENTITY_PK PRIMARY KEY (workspace, id)
|
||||||
)"""
|
)"""
|
||||||
},
|
},
|
||||||
@@ -1586,7 +1586,7 @@ TABLES = {
|
|||||||
content_vector VECTOR,
|
content_vector VECTOR,
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time TIMESTAMP,
|
update_time TIMESTAMP,
|
||||||
chunk_ids VARCHAR(255)[] NULL,
|
chunk_id TEXT NULL,
|
||||||
CONSTRAINT LIGHTRAG_VDB_RELATION_PK PRIMARY KEY (workspace, id)
|
CONSTRAINT LIGHTRAG_VDB_RELATION_PK PRIMARY KEY (workspace, id)
|
||||||
)"""
|
)"""
|
||||||
},
|
},
|
||||||
|
@@ -3,11 +3,14 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
import csv
|
||||||
import warnings
|
import warnings
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any, AsyncIterator, Callable, Iterator, cast, final
|
from typing import Any, AsyncIterator, Callable, Iterator, cast, final, Literal
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
from lightrag.kg import (
|
from lightrag.kg import (
|
||||||
STORAGE_ENV_REQUIREMENTS,
|
STORAGE_ENV_REQUIREMENTS,
|
||||||
@@ -211,7 +214,7 @@ class LightRAG:
|
|||||||
llm_model_max_token_size: int = field(default=int(os.getenv("MAX_TOKENS", 32768)))
|
llm_model_max_token_size: int = field(default=int(os.getenv("MAX_TOKENS", 32768)))
|
||||||
"""Maximum number of tokens allowed per LLM response."""
|
"""Maximum number of tokens allowed per LLM response."""
|
||||||
|
|
||||||
llm_model_max_async: int = field(default=int(os.getenv("MAX_ASYNC", 16)))
|
llm_model_max_async: int = field(default=int(os.getenv("MAX_ASYNC", 4)))
|
||||||
"""Maximum number of concurrent LLM calls."""
|
"""Maximum number of concurrent LLM calls."""
|
||||||
|
|
||||||
llm_model_kwargs: dict[str, Any] = field(default_factory=dict)
|
llm_model_kwargs: dict[str, Any] = field(default_factory=dict)
|
||||||
@@ -235,7 +238,7 @@ class LightRAG:
|
|||||||
# Extensions
|
# Extensions
|
||||||
# ---
|
# ---
|
||||||
|
|
||||||
max_parallel_insert: int = field(default=int(os.getenv("MAX_PARALLEL_INSERT", 20)))
|
max_parallel_insert: int = field(default=int(os.getenv("MAX_PARALLEL_INSERT", 2)))
|
||||||
"""Maximum number of parallel insert operations."""
|
"""Maximum number of parallel insert operations."""
|
||||||
|
|
||||||
addon_params: dict[str, Any] = field(
|
addon_params: dict[str, Any] = field(
|
||||||
@@ -550,6 +553,7 @@ class LightRAG:
|
|||||||
Args:
|
Args:
|
||||||
input: Single document string or list of document strings
|
input: Single document string or list of document strings
|
||||||
split_by_character: if split_by_character is not None, split the string by character, if chunk longer than
|
split_by_character: if split_by_character is not None, split the string by character, if chunk longer than
|
||||||
|
chunk_token_size, it will be split again by token size.
|
||||||
split_by_character_only: if split_by_character_only is True, split the string by character only, when
|
split_by_character_only: if split_by_character_only is True, split the string by character only, when
|
||||||
split_by_character is None, this parameter is ignored.
|
split_by_character is None, this parameter is ignored.
|
||||||
ids: single string of the document ID or list of unique document IDs, if not provided, MD5 hash IDs will be generated
|
ids: single string of the document ID or list of unique document IDs, if not provided, MD5 hash IDs will be generated
|
||||||
@@ -571,6 +575,7 @@ class LightRAG:
|
|||||||
Args:
|
Args:
|
||||||
input: Single document string or list of document strings
|
input: Single document string or list of document strings
|
||||||
split_by_character: if split_by_character is not None, split the string by character, if chunk longer than
|
split_by_character: if split_by_character is not None, split the string by character, if chunk longer than
|
||||||
|
chunk_token_size, it will be split again by token size.
|
||||||
split_by_character_only: if split_by_character_only is True, split the string by character only, when
|
split_by_character_only: if split_by_character_only is True, split the string by character only, when
|
||||||
split_by_character is None, this parameter is ignored.
|
split_by_character is None, this parameter is ignored.
|
||||||
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
|
ids: list of unique document IDs, if not provided, MD5 hash IDs will be generated
|
||||||
@@ -764,7 +769,6 @@ class LightRAG:
|
|||||||
async with pipeline_status_lock:
|
async with pipeline_status_lock:
|
||||||
# Ensure only one worker is processing documents
|
# Ensure only one worker is processing documents
|
||||||
if not pipeline_status.get("busy", False):
|
if not pipeline_status.get("busy", False):
|
||||||
# 先检查是否有需要处理的文档
|
|
||||||
processing_docs, failed_docs, pending_docs = await asyncio.gather(
|
processing_docs, failed_docs, pending_docs = await asyncio.gather(
|
||||||
self.doc_status.get_docs_by_status(DocStatus.PROCESSING),
|
self.doc_status.get_docs_by_status(DocStatus.PROCESSING),
|
||||||
self.doc_status.get_docs_by_status(DocStatus.FAILED),
|
self.doc_status.get_docs_by_status(DocStatus.FAILED),
|
||||||
@@ -776,12 +780,10 @@ class LightRAG:
|
|||||||
to_process_docs.update(failed_docs)
|
to_process_docs.update(failed_docs)
|
||||||
to_process_docs.update(pending_docs)
|
to_process_docs.update(pending_docs)
|
||||||
|
|
||||||
# 如果没有需要处理的文档,直接返回,保留 pipeline_status 中的内容不变
|
|
||||||
if not to_process_docs:
|
if not to_process_docs:
|
||||||
logger.info("No documents to process")
|
logger.info("No documents to process")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 有文档需要处理,更新 pipeline_status
|
|
||||||
pipeline_status.update(
|
pipeline_status.update(
|
||||||
{
|
{
|
||||||
"busy": True,
|
"busy": True,
|
||||||
@@ -820,7 +822,7 @@ class LightRAG:
|
|||||||
for i in range(0, len(to_process_docs), self.max_parallel_insert)
|
for i in range(0, len(to_process_docs), self.max_parallel_insert)
|
||||||
]
|
]
|
||||||
|
|
||||||
log_message = f"Number of batches to process: {len(docs_batches)}."
|
log_message = f"Processing {len(to_process_docs)} document(s) in {len(docs_batches)} batches"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
|
||||||
# Update pipeline status with current batch information
|
# Update pipeline status with current batch information
|
||||||
@@ -829,140 +831,149 @@ class LightRAG:
|
|||||||
pipeline_status["latest_message"] = log_message
|
pipeline_status["latest_message"] = log_message
|
||||||
pipeline_status["history_messages"].append(log_message)
|
pipeline_status["history_messages"].append(log_message)
|
||||||
|
|
||||||
batches: list[Any] = []
|
async def process_document(
|
||||||
# 3. iterate over batches
|
doc_id: str,
|
||||||
for batch_idx, docs_batch in enumerate(docs_batches):
|
status_doc: DocProcessingStatus,
|
||||||
# Update current batch in pipeline status (directly, as it's atomic)
|
split_by_character: str | None,
|
||||||
pipeline_status["cur_batch"] += 1
|
split_by_character_only: bool,
|
||||||
|
pipeline_status: dict,
|
||||||
async def batch(
|
pipeline_status_lock: asyncio.Lock,
|
||||||
batch_idx: int,
|
) -> None:
|
||||||
docs_batch: list[tuple[str, DocProcessingStatus]],
|
"""Process single document"""
|
||||||
size_batch: int,
|
try:
|
||||||
) -> None:
|
# Generate chunks from document
|
||||||
log_message = (
|
chunks: dict[str, Any] = {
|
||||||
f"Start processing batch {batch_idx + 1} of {size_batch}."
|
compute_mdhash_id(dp["content"], prefix="chunk-"): {
|
||||||
)
|
**dp,
|
||||||
logger.info(log_message)
|
"full_doc_id": doc_id,
|
||||||
pipeline_status["latest_message"] = log_message
|
|
||||||
pipeline_status["history_messages"].append(log_message)
|
|
||||||
# 4. iterate over batch
|
|
||||||
for doc_id_processing_status in docs_batch:
|
|
||||||
doc_id, status_doc = doc_id_processing_status
|
|
||||||
# Generate chunks from document
|
|
||||||
chunks: dict[str, Any] = {
|
|
||||||
compute_mdhash_id(dp["content"], prefix="chunk-"): {
|
|
||||||
**dp,
|
|
||||||
"full_doc_id": doc_id,
|
|
||||||
}
|
|
||||||
for dp in self.chunking_func(
|
|
||||||
status_doc.content,
|
|
||||||
split_by_character,
|
|
||||||
split_by_character_only,
|
|
||||||
self.chunk_overlap_token_size,
|
|
||||||
self.chunk_token_size,
|
|
||||||
self.tiktoken_model_name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
# Process document (text chunks and full docs) in parallel
|
for dp in self.chunking_func(
|
||||||
# Create tasks with references for potential cancellation
|
status_doc.content,
|
||||||
doc_status_task = asyncio.create_task(
|
split_by_character,
|
||||||
self.doc_status.upsert(
|
split_by_character_only,
|
||||||
{
|
self.chunk_overlap_token_size,
|
||||||
doc_id: {
|
self.chunk_token_size,
|
||||||
"status": DocStatus.PROCESSING,
|
self.tiktoken_model_name,
|
||||||
"updated_at": datetime.now().isoformat(),
|
)
|
||||||
"content": status_doc.content,
|
}
|
||||||
"content_summary": status_doc.content_summary,
|
# Process document (text chunks and full docs) in parallel
|
||||||
"content_length": status_doc.content_length,
|
# Create tasks with references for potential cancellation
|
||||||
"created_at": status_doc.created_at,
|
doc_status_task = asyncio.create_task(
|
||||||
}
|
self.doc_status.upsert(
|
||||||
|
{
|
||||||
|
doc_id: {
|
||||||
|
"status": DocStatus.PROCESSING,
|
||||||
|
"updated_at": datetime.now().isoformat(),
|
||||||
|
"content": status_doc.content,
|
||||||
|
"content_summary": status_doc.content_summary,
|
||||||
|
"content_length": status_doc.content_length,
|
||||||
|
"created_at": status_doc.created_at,
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
)
|
)
|
||||||
chunks_vdb_task = asyncio.create_task(
|
)
|
||||||
self.chunks_vdb.upsert(chunks)
|
chunks_vdb_task = asyncio.create_task(
|
||||||
|
self.chunks_vdb.upsert(chunks)
|
||||||
|
)
|
||||||
|
entity_relation_task = asyncio.create_task(
|
||||||
|
self._process_entity_relation_graph(
|
||||||
|
chunks, pipeline_status, pipeline_status_lock
|
||||||
)
|
)
|
||||||
entity_relation_task = asyncio.create_task(
|
)
|
||||||
self._process_entity_relation_graph(
|
full_docs_task = asyncio.create_task(
|
||||||
chunks, pipeline_status, pipeline_status_lock
|
self.full_docs.upsert(
|
||||||
)
|
{doc_id: {"content": status_doc.content}}
|
||||||
)
|
)
|
||||||
full_docs_task = asyncio.create_task(
|
)
|
||||||
self.full_docs.upsert(
|
text_chunks_task = asyncio.create_task(
|
||||||
{doc_id: {"content": status_doc.content}}
|
self.text_chunks.upsert(chunks)
|
||||||
)
|
)
|
||||||
)
|
tasks = [
|
||||||
text_chunks_task = asyncio.create_task(
|
doc_status_task,
|
||||||
self.text_chunks.upsert(chunks)
|
chunks_vdb_task,
|
||||||
)
|
entity_relation_task,
|
||||||
tasks = [
|
full_docs_task,
|
||||||
doc_status_task,
|
text_chunks_task,
|
||||||
|
]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
await self.doc_status.upsert(
|
||||||
|
{
|
||||||
|
doc_id: {
|
||||||
|
"status": DocStatus.PROCESSED,
|
||||||
|
"chunks_count": len(chunks),
|
||||||
|
"content": status_doc.content,
|
||||||
|
"content_summary": status_doc.content_summary,
|
||||||
|
"content_length": status_doc.content_length,
|
||||||
|
"created_at": status_doc.created_at,
|
||||||
|
"updated_at": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Log error and update pipeline status
|
||||||
|
error_msg = f"Failed to process document {doc_id}: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
async with pipeline_status_lock:
|
||||||
|
pipeline_status["latest_message"] = error_msg
|
||||||
|
pipeline_status["history_messages"].append(error_msg)
|
||||||
|
|
||||||
|
# Cancel other tasks as they are no longer meaningful
|
||||||
|
for task in [
|
||||||
chunks_vdb_task,
|
chunks_vdb_task,
|
||||||
entity_relation_task,
|
entity_relation_task,
|
||||||
full_docs_task,
|
full_docs_task,
|
||||||
text_chunks_task,
|
text_chunks_task,
|
||||||
]
|
]:
|
||||||
try:
|
if not task.done():
|
||||||
await asyncio.gather(*tasks)
|
task.cancel()
|
||||||
await self.doc_status.upsert(
|
# Update document status to failed
|
||||||
{
|
await self.doc_status.upsert(
|
||||||
doc_id: {
|
{
|
||||||
"status": DocStatus.PROCESSED,
|
doc_id: {
|
||||||
"chunks_count": len(chunks),
|
"status": DocStatus.FAILED,
|
||||||
"content": status_doc.content,
|
"error": str(e),
|
||||||
"content_summary": status_doc.content_summary,
|
"content": status_doc.content,
|
||||||
"content_length": status_doc.content_length,
|
"content_summary": status_doc.content_summary,
|
||||||
"created_at": status_doc.created_at,
|
"content_length": status_doc.content_length,
|
||||||
"updated_at": datetime.now().isoformat(),
|
"created_at": status_doc.created_at,
|
||||||
}
|
"updated_at": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
except Exception as e:
|
|
||||||
# Log error and update pipeline status
|
|
||||||
error_msg = (
|
|
||||||
f"Failed to process document {doc_id}: {str(e)}"
|
|
||||||
)
|
|
||||||
logger.error(error_msg)
|
|
||||||
pipeline_status["latest_message"] = error_msg
|
|
||||||
pipeline_status["history_messages"].append(error_msg)
|
|
||||||
|
|
||||||
# Cancel other tasks as they are no longer meaningful
|
|
||||||
for task in [
|
|
||||||
chunks_vdb_task,
|
|
||||||
entity_relation_task,
|
|
||||||
full_docs_task,
|
|
||||||
text_chunks_task,
|
|
||||||
]:
|
|
||||||
if not task.done():
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
# Update document status to failed
|
|
||||||
await self.doc_status.upsert(
|
|
||||||
{
|
|
||||||
doc_id: {
|
|
||||||
"status": DocStatus.FAILED,
|
|
||||||
"error": str(e),
|
|
||||||
"content": status_doc.content,
|
|
||||||
"content_summary": status_doc.content_summary,
|
|
||||||
"content_length": status_doc.content_length,
|
|
||||||
"created_at": status_doc.created_at,
|
|
||||||
"updated_at": datetime.now().isoformat(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
log_message = (
|
|
||||||
f"Completed batch {batch_idx + 1} of {len(docs_batches)}."
|
|
||||||
)
|
)
|
||||||
logger.info(log_message)
|
|
||||||
pipeline_status["latest_message"] = log_message
|
|
||||||
pipeline_status["history_messages"].append(log_message)
|
|
||||||
|
|
||||||
batches.append(batch(batch_idx, docs_batch, len(docs_batches)))
|
# 3. iterate over batches
|
||||||
|
total_batches = len(docs_batches)
|
||||||
|
for batch_idx, docs_batch in enumerate(docs_batches):
|
||||||
|
current_batch = batch_idx + 1
|
||||||
|
log_message = (
|
||||||
|
f"Start processing batch {current_batch} of {total_batches}."
|
||||||
|
)
|
||||||
|
logger.info(log_message)
|
||||||
|
pipeline_status["cur_batch"] = current_batch
|
||||||
|
pipeline_status["latest_message"] = log_message
|
||||||
|
pipeline_status["history_messages"].append(log_message)
|
||||||
|
|
||||||
await asyncio.gather(*batches)
|
doc_tasks = []
|
||||||
await self._insert_done()
|
for doc_id, status_doc in docs_batch:
|
||||||
|
doc_tasks.append(
|
||||||
|
process_document(
|
||||||
|
doc_id,
|
||||||
|
status_doc,
|
||||||
|
split_by_character,
|
||||||
|
split_by_character_only,
|
||||||
|
pipeline_status,
|
||||||
|
pipeline_status_lock,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process documents in one batch parallelly
|
||||||
|
await asyncio.gather(*doc_tasks)
|
||||||
|
await self._insert_done()
|
||||||
|
|
||||||
|
log_message = f"Completed batch {current_batch} of {total_batches}."
|
||||||
|
logger.info(log_message)
|
||||||
|
pipeline_status["latest_message"] = log_message
|
||||||
|
pipeline_status["history_messages"].append(log_message)
|
||||||
|
|
||||||
# Check if there's a pending request to process more documents (with lock)
|
# Check if there's a pending request to process more documents (with lock)
|
||||||
has_pending_request = False
|
has_pending_request = False
|
||||||
@@ -1037,7 +1048,7 @@ class LightRAG:
|
|||||||
]
|
]
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
log_message = "All Insert done"
|
log_message = "In memory DB persist to disk"
|
||||||
logger.info(log_message)
|
logger.info(log_message)
|
||||||
|
|
||||||
if pipeline_status is not None and pipeline_status_lock is not None:
|
if pipeline_status is not None and pipeline_status_lock is not None:
|
||||||
@@ -1111,6 +1122,7 @@ class LightRAG:
|
|||||||
|
|
||||||
# Prepare node data
|
# Prepare node data
|
||||||
node_data: dict[str, str] = {
|
node_data: dict[str, str] = {
|
||||||
|
"entity_id": entity_name,
|
||||||
"entity_type": entity_type,
|
"entity_type": entity_type,
|
||||||
"description": description,
|
"description": description,
|
||||||
"source_id": source_id,
|
"source_id": source_id,
|
||||||
@@ -1148,6 +1160,7 @@ class LightRAG:
|
|||||||
await self.chunk_entity_relation_graph.upsert_node(
|
await self.chunk_entity_relation_graph.upsert_node(
|
||||||
need_insert_id,
|
need_insert_id,
|
||||||
node_data={
|
node_data={
|
||||||
|
"entity_id": need_insert_id,
|
||||||
"source_id": source_id,
|
"source_id": source_id,
|
||||||
"description": "UNKNOWN",
|
"description": "UNKNOWN",
|
||||||
"entity_type": "UNKNOWN",
|
"entity_type": "UNKNOWN",
|
||||||
@@ -1838,9 +1851,10 @@ class LightRAG:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. Get current entity information
|
# 1. Get current entity information
|
||||||
node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
|
node_exists = await self.chunk_entity_relation_graph.has_node(entity_name)
|
||||||
if not node_data:
|
if not node_exists:
|
||||||
raise ValueError(f"Entity '{entity_name}' does not exist")
|
raise ValueError(f"Entity '{entity_name}' does not exist")
|
||||||
|
node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
|
||||||
|
|
||||||
# Check if entity is being renamed
|
# Check if entity is being renamed
|
||||||
new_entity_name = updated_data.get("entity_name", entity_name)
|
new_entity_name = updated_data.get("entity_name", entity_name)
|
||||||
@@ -1853,7 +1867,7 @@ class LightRAG:
|
|||||||
"Entity renaming is not allowed. Set allow_rename=True to enable this feature"
|
"Entity renaming is not allowed. Set allow_rename=True to enable this feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_node = await self.chunk_entity_relation_graph.get_node(
|
existing_node = await self.chunk_entity_relation_graph.has_node(
|
||||||
new_entity_name
|
new_entity_name
|
||||||
)
|
)
|
||||||
if existing_node:
|
if existing_node:
|
||||||
@@ -2035,14 +2049,16 @@ class LightRAG:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. Get current relation information
|
# 1. Get current relation information
|
||||||
edge_data = await self.chunk_entity_relation_graph.get_edge(
|
edge_exists = await self.chunk_entity_relation_graph.has_edge(
|
||||||
source_entity, target_entity
|
source_entity, target_entity
|
||||||
)
|
)
|
||||||
if not edge_data:
|
if not edge_exists:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Relation from '{source_entity}' to '{target_entity}' does not exist"
|
f"Relation from '{source_entity}' to '{target_entity}' does not exist"
|
||||||
)
|
)
|
||||||
|
edge_data = await self.chunk_entity_relation_graph.get_edge(
|
||||||
|
source_entity, target_entity
|
||||||
|
)
|
||||||
# Important: First delete the old relation record from the vector database
|
# Important: First delete the old relation record from the vector database
|
||||||
old_relation_id = compute_mdhash_id(
|
old_relation_id = compute_mdhash_id(
|
||||||
source_entity + target_entity, prefix="rel-"
|
source_entity + target_entity, prefix="rel-"
|
||||||
@@ -2151,12 +2167,13 @@ class LightRAG:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Check if entity already exists
|
# Check if entity already exists
|
||||||
existing_node = await self.chunk_entity_relation_graph.get_node(entity_name)
|
existing_node = await self.chunk_entity_relation_graph.has_node(entity_name)
|
||||||
if existing_node:
|
if existing_node:
|
||||||
raise ValueError(f"Entity '{entity_name}' already exists")
|
raise ValueError(f"Entity '{entity_name}' already exists")
|
||||||
|
|
||||||
# Prepare node data with defaults if missing
|
# Prepare node data with defaults if missing
|
||||||
node_data = {
|
node_data = {
|
||||||
|
"entity_id": entity_name,
|
||||||
"entity_type": entity_data.get("entity_type", "UNKNOWN"),
|
"entity_type": entity_data.get("entity_type", "UNKNOWN"),
|
||||||
"description": entity_data.get("description", ""),
|
"description": entity_data.get("description", ""),
|
||||||
"source_id": entity_data.get("source_id", "manual"),
|
"source_id": entity_data.get("source_id", "manual"),
|
||||||
@@ -2244,7 +2261,7 @@ class LightRAG:
|
|||||||
raise ValueError(f"Target entity '{target_entity}' does not exist")
|
raise ValueError(f"Target entity '{target_entity}' does not exist")
|
||||||
|
|
||||||
# Check if relation already exists
|
# Check if relation already exists
|
||||||
existing_edge = await self.chunk_entity_relation_graph.get_edge(
|
existing_edge = await self.chunk_entity_relation_graph.has_edge(
|
||||||
source_entity, target_entity
|
source_entity, target_entity
|
||||||
)
|
)
|
||||||
if existing_edge:
|
if existing_edge:
|
||||||
@@ -2377,19 +2394,22 @@ class LightRAG:
|
|||||||
# 1. Check if all source entities exist
|
# 1. Check if all source entities exist
|
||||||
source_entities_data = {}
|
source_entities_data = {}
|
||||||
for entity_name in source_entities:
|
for entity_name in source_entities:
|
||||||
node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
|
node_exists = await self.chunk_entity_relation_graph.has_node(
|
||||||
if not node_data:
|
entity_name
|
||||||
|
)
|
||||||
|
if not node_exists:
|
||||||
raise ValueError(f"Source entity '{entity_name}' does not exist")
|
raise ValueError(f"Source entity '{entity_name}' does not exist")
|
||||||
|
node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
|
||||||
source_entities_data[entity_name] = node_data
|
source_entities_data[entity_name] = node_data
|
||||||
|
|
||||||
# 2. Check if target entity exists and get its data if it does
|
# 2. Check if target entity exists and get its data if it does
|
||||||
target_exists = await self.chunk_entity_relation_graph.has_node(
|
target_exists = await self.chunk_entity_relation_graph.has_node(
|
||||||
target_entity
|
target_entity
|
||||||
)
|
)
|
||||||
target_entity_data = {}
|
existing_target_entity_data = {}
|
||||||
if target_exists:
|
if target_exists:
|
||||||
target_entity_data = await self.chunk_entity_relation_graph.get_node(
|
existing_target_entity_data = (
|
||||||
target_entity
|
await self.chunk_entity_relation_graph.get_node(target_entity)
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Target entity '{target_entity}' already exists, will merge data"
|
f"Target entity '{target_entity}' already exists, will merge data"
|
||||||
@@ -2398,7 +2418,7 @@ class LightRAG:
|
|||||||
# 3. Merge entity data
|
# 3. Merge entity data
|
||||||
merged_entity_data = self._merge_entity_attributes(
|
merged_entity_data = self._merge_entity_attributes(
|
||||||
list(source_entities_data.values())
|
list(source_entities_data.values())
|
||||||
+ ([target_entity_data] if target_exists else []),
|
+ ([existing_target_entity_data] if target_exists else []),
|
||||||
merge_strategy,
|
merge_strategy,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2592,6 +2612,322 @@ class LightRAG:
|
|||||||
logger.error(f"Error merging entities: {e}")
|
logger.error(f"Error merging entities: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def aexport_data(
|
||||||
|
self,
|
||||||
|
output_path: str,
|
||||||
|
file_format: Literal["csv", "excel", "md", "txt"] = "csv",
|
||||||
|
include_vector_data: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Asynchronously exports all entities, relations, and relationships to various formats.
|
||||||
|
Args:
|
||||||
|
output_path: The path to the output file (including extension).
|
||||||
|
file_format: Output format - "csv", "excel", "md", "txt".
|
||||||
|
- csv: Comma-separated values file
|
||||||
|
- excel: Microsoft Excel file with multiple sheets
|
||||||
|
- md: Markdown tables
|
||||||
|
- txt: Plain text formatted output
|
||||||
|
- table: Print formatted tables to console
|
||||||
|
include_vector_data: Whether to include data from the vector database.
|
||||||
|
"""
|
||||||
|
# Collect data
|
||||||
|
entities_data = []
|
||||||
|
relations_data = []
|
||||||
|
relationships_data = []
|
||||||
|
|
||||||
|
# --- Entities ---
|
||||||
|
all_entities = await self.chunk_entity_relation_graph.get_all_labels()
|
||||||
|
for entity_name in all_entities:
|
||||||
|
entity_info = await self.get_entity_info(
|
||||||
|
entity_name, include_vector_data=include_vector_data
|
||||||
|
)
|
||||||
|
entity_row = {
|
||||||
|
"entity_name": entity_name,
|
||||||
|
"source_id": entity_info["source_id"],
|
||||||
|
"graph_data": str(
|
||||||
|
entity_info["graph_data"]
|
||||||
|
), # Convert to string to ensure compatibility
|
||||||
|
}
|
||||||
|
if include_vector_data and "vector_data" in entity_info:
|
||||||
|
entity_row["vector_data"] = str(entity_info["vector_data"])
|
||||||
|
entities_data.append(entity_row)
|
||||||
|
|
||||||
|
# --- Relations ---
|
||||||
|
for src_entity in all_entities:
|
||||||
|
for tgt_entity in all_entities:
|
||||||
|
if src_entity == tgt_entity:
|
||||||
|
continue
|
||||||
|
|
||||||
|
edge_exists = await self.chunk_entity_relation_graph.has_edge(
|
||||||
|
src_entity, tgt_entity
|
||||||
|
)
|
||||||
|
if edge_exists:
|
||||||
|
relation_info = await self.get_relation_info(
|
||||||
|
src_entity, tgt_entity, include_vector_data=include_vector_data
|
||||||
|
)
|
||||||
|
relation_row = {
|
||||||
|
"src_entity": src_entity,
|
||||||
|
"tgt_entity": tgt_entity,
|
||||||
|
"source_id": relation_info["source_id"],
|
||||||
|
"graph_data": str(
|
||||||
|
relation_info["graph_data"]
|
||||||
|
), # Convert to string
|
||||||
|
}
|
||||||
|
if include_vector_data and "vector_data" in relation_info:
|
||||||
|
relation_row["vector_data"] = str(relation_info["vector_data"])
|
||||||
|
relations_data.append(relation_row)
|
||||||
|
|
||||||
|
# --- Relationships (from VectorDB) ---
|
||||||
|
all_relationships = await self.relationships_vdb.client_storage
|
||||||
|
for rel in all_relationships["data"]:
|
||||||
|
relationships_data.append(
|
||||||
|
{
|
||||||
|
"relationship_id": rel["__id__"],
|
||||||
|
"data": str(rel), # Convert to string for compatibility
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Export based on format
|
||||||
|
if file_format == "csv":
|
||||||
|
# CSV export
|
||||||
|
with open(output_path, "w", newline="", encoding="utf-8") as csvfile:
|
||||||
|
# Entities
|
||||||
|
if entities_data:
|
||||||
|
csvfile.write("# ENTITIES\n")
|
||||||
|
writer = csv.DictWriter(csvfile, fieldnames=entities_data[0].keys())
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(entities_data)
|
||||||
|
csvfile.write("\n\n")
|
||||||
|
|
||||||
|
# Relations
|
||||||
|
if relations_data:
|
||||||
|
csvfile.write("# RELATIONS\n")
|
||||||
|
writer = csv.DictWriter(
|
||||||
|
csvfile, fieldnames=relations_data[0].keys()
|
||||||
|
)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(relations_data)
|
||||||
|
csvfile.write("\n\n")
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
if relationships_data:
|
||||||
|
csvfile.write("# RELATIONSHIPS\n")
|
||||||
|
writer = csv.DictWriter(
|
||||||
|
csvfile, fieldnames=relationships_data[0].keys()
|
||||||
|
)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(relationships_data)
|
||||||
|
|
||||||
|
elif file_format == "excel":
|
||||||
|
# Excel export
|
||||||
|
entities_df = (
|
||||||
|
pd.DataFrame(entities_data) if entities_data else pd.DataFrame()
|
||||||
|
)
|
||||||
|
relations_df = (
|
||||||
|
pd.DataFrame(relations_data) if relations_data else pd.DataFrame()
|
||||||
|
)
|
||||||
|
relationships_df = (
|
||||||
|
pd.DataFrame(relationships_data)
|
||||||
|
if relationships_data
|
||||||
|
else pd.DataFrame()
|
||||||
|
)
|
||||||
|
|
||||||
|
with pd.ExcelWriter(output_path, engine="xlsxwriter") as writer:
|
||||||
|
if not entities_df.empty:
|
||||||
|
entities_df.to_excel(writer, sheet_name="Entities", index=False)
|
||||||
|
if not relations_df.empty:
|
||||||
|
relations_df.to_excel(writer, sheet_name="Relations", index=False)
|
||||||
|
if not relationships_df.empty:
|
||||||
|
relationships_df.to_excel(
|
||||||
|
writer, sheet_name="Relationships", index=False
|
||||||
|
)
|
||||||
|
|
||||||
|
elif file_format == "md":
|
||||||
|
# Markdown export
|
||||||
|
with open(output_path, "w", encoding="utf-8") as mdfile:
|
||||||
|
mdfile.write("# LightRAG Data Export\n\n")
|
||||||
|
|
||||||
|
# Entities
|
||||||
|
mdfile.write("## Entities\n\n")
|
||||||
|
if entities_data:
|
||||||
|
# Write header
|
||||||
|
mdfile.write("| " + " | ".join(entities_data[0].keys()) + " |\n")
|
||||||
|
mdfile.write(
|
||||||
|
"| "
|
||||||
|
+ " | ".join(["---"] * len(entities_data[0].keys()))
|
||||||
|
+ " |\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write rows
|
||||||
|
for entity in entities_data:
|
||||||
|
mdfile.write(
|
||||||
|
"| " + " | ".join(str(v) for v in entity.values()) + " |\n"
|
||||||
|
)
|
||||||
|
mdfile.write("\n\n")
|
||||||
|
else:
|
||||||
|
mdfile.write("*No entity data available*\n\n")
|
||||||
|
|
||||||
|
# Relations
|
||||||
|
mdfile.write("## Relations\n\n")
|
||||||
|
if relations_data:
|
||||||
|
# Write header
|
||||||
|
mdfile.write("| " + " | ".join(relations_data[0].keys()) + " |\n")
|
||||||
|
mdfile.write(
|
||||||
|
"| "
|
||||||
|
+ " | ".join(["---"] * len(relations_data[0].keys()))
|
||||||
|
+ " |\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write rows
|
||||||
|
for relation in relations_data:
|
||||||
|
mdfile.write(
|
||||||
|
"| "
|
||||||
|
+ " | ".join(str(v) for v in relation.values())
|
||||||
|
+ " |\n"
|
||||||
|
)
|
||||||
|
mdfile.write("\n\n")
|
||||||
|
else:
|
||||||
|
mdfile.write("*No relation data available*\n\n")
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
mdfile.write("## Relationships\n\n")
|
||||||
|
if relationships_data:
|
||||||
|
# Write header
|
||||||
|
mdfile.write(
|
||||||
|
"| " + " | ".join(relationships_data[0].keys()) + " |\n"
|
||||||
|
)
|
||||||
|
mdfile.write(
|
||||||
|
"| "
|
||||||
|
+ " | ".join(["---"] * len(relationships_data[0].keys()))
|
||||||
|
+ " |\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write rows
|
||||||
|
for relationship in relationships_data:
|
||||||
|
mdfile.write(
|
||||||
|
"| "
|
||||||
|
+ " | ".join(str(v) for v in relationship.values())
|
||||||
|
+ " |\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
mdfile.write("*No relationship data available*\n\n")
|
||||||
|
|
||||||
|
elif file_format == "txt":
|
||||||
|
# Plain text export
|
||||||
|
with open(output_path, "w", encoding="utf-8") as txtfile:
|
||||||
|
txtfile.write("LIGHTRAG DATA EXPORT\n")
|
||||||
|
txtfile.write("=" * 80 + "\n\n")
|
||||||
|
|
||||||
|
# Entities
|
||||||
|
txtfile.write("ENTITIES\n")
|
||||||
|
txtfile.write("-" * 80 + "\n")
|
||||||
|
if entities_data:
|
||||||
|
# Create fixed width columns
|
||||||
|
col_widths = {
|
||||||
|
k: max(len(k), max(len(str(e[k])) for e in entities_data))
|
||||||
|
for k in entities_data[0]
|
||||||
|
}
|
||||||
|
header = " ".join(k.ljust(col_widths[k]) for k in entities_data[0])
|
||||||
|
txtfile.write(header + "\n")
|
||||||
|
txtfile.write("-" * len(header) + "\n")
|
||||||
|
|
||||||
|
# Write rows
|
||||||
|
for entity in entities_data:
|
||||||
|
row = " ".join(
|
||||||
|
str(v).ljust(col_widths[k]) for k, v in entity.items()
|
||||||
|
)
|
||||||
|
txtfile.write(row + "\n")
|
||||||
|
txtfile.write("\n\n")
|
||||||
|
else:
|
||||||
|
txtfile.write("No entity data available\n\n")
|
||||||
|
|
||||||
|
# Relations
|
||||||
|
txtfile.write("RELATIONS\n")
|
||||||
|
txtfile.write("-" * 80 + "\n")
|
||||||
|
if relations_data:
|
||||||
|
# Create fixed width columns
|
||||||
|
col_widths = {
|
||||||
|
k: max(len(k), max(len(str(r[k])) for r in relations_data))
|
||||||
|
for k in relations_data[0]
|
||||||
|
}
|
||||||
|
header = " ".join(
|
||||||
|
k.ljust(col_widths[k]) for k in relations_data[0]
|
||||||
|
)
|
||||||
|
txtfile.write(header + "\n")
|
||||||
|
txtfile.write("-" * len(header) + "\n")
|
||||||
|
|
||||||
|
# Write rows
|
||||||
|
for relation in relations_data:
|
||||||
|
row = " ".join(
|
||||||
|
str(v).ljust(col_widths[k]) for k, v in relation.items()
|
||||||
|
)
|
||||||
|
txtfile.write(row + "\n")
|
||||||
|
txtfile.write("\n\n")
|
||||||
|
else:
|
||||||
|
txtfile.write("No relation data available\n\n")
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
txtfile.write("RELATIONSHIPS\n")
|
||||||
|
txtfile.write("-" * 80 + "\n")
|
||||||
|
if relationships_data:
|
||||||
|
# Create fixed width columns
|
||||||
|
col_widths = {
|
||||||
|
k: max(len(k), max(len(str(r[k])) for r in relationships_data))
|
||||||
|
for k in relationships_data[0]
|
||||||
|
}
|
||||||
|
header = " ".join(
|
||||||
|
k.ljust(col_widths[k]) for k in relationships_data[0]
|
||||||
|
)
|
||||||
|
txtfile.write(header + "\n")
|
||||||
|
txtfile.write("-" * len(header) + "\n")
|
||||||
|
|
||||||
|
# Write rows
|
||||||
|
for relationship in relationships_data:
|
||||||
|
row = " ".join(
|
||||||
|
str(v).ljust(col_widths[k]) for k, v in relationship.items()
|
||||||
|
)
|
||||||
|
txtfile.write(row + "\n")
|
||||||
|
else:
|
||||||
|
txtfile.write("No relationship data available\n\n")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported file format: {file_format}. "
|
||||||
|
f"Choose from: csv, excel, md, txt"
|
||||||
|
)
|
||||||
|
if file_format is not None:
|
||||||
|
print(f"Data exported to: {output_path} with format: {file_format}")
|
||||||
|
else:
|
||||||
|
print("Data displayed as table format")
|
||||||
|
|
||||||
|
def export_data(
|
||||||
|
self,
|
||||||
|
output_path: str,
|
||||||
|
file_format: Literal["csv", "excel", "md", "txt"] = "csv",
|
||||||
|
include_vector_data: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Synchronously exports all entities, relations, and relationships to various formats.
|
||||||
|
Args:
|
||||||
|
output_path: The path to the output file (including extension).
|
||||||
|
file_format: Output format - "csv", "excel", "md", "txt".
|
||||||
|
- csv: Comma-separated values file
|
||||||
|
- excel: Microsoft Excel file with multiple sheets
|
||||||
|
- md: Markdown tables
|
||||||
|
- txt: Plain text formatted output
|
||||||
|
- table: Print formatted tables to console
|
||||||
|
include_vector_data: Whether to include data from the vector database.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
loop.run_until_complete(
|
||||||
|
self.aexport_data(output_path, file_format, include_vector_data)
|
||||||
|
)
|
||||||
|
|
||||||
def merge_entities(
|
def merge_entities(
|
||||||
self,
|
self,
|
||||||
source_entities: list[str],
|
source_entities: list[str],
|
||||||
|
@@ -76,6 +76,7 @@ class LightragPathFilter(logging.Filter):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
# Define paths to be filtered
|
# Define paths to be filtered
|
||||||
self.filtered_paths = ["/documents", "/health", "/webui/"]
|
self.filtered_paths = ["/documents", "/health", "/webui/"]
|
||||||
|
# self.filtered_paths = ["/health", "/webui/"]
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
try:
|
try:
|
||||||
|
@@ -62,6 +62,7 @@
|
|||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@types/react-i18next": "^8.1.0",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
@@ -443,6 +444,8 @@
|
|||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
|
"@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
|
||||||
|
|
||||||
|
"@types/react-i18next": ["@types/react-i18next@8.1.0", "", { "dependencies": { "react-i18next": "*" } }, "sha512-d4xhcjX5b3roNMObRNMfb1HinHQlQLPo8xlDj60dnHeeAw2bBymR2cy/l1giJpHzo/ZFgSvgVUvIWr4kCrenCg=="],
|
||||||
|
|
||||||
"@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
|
"@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
|
||||||
|
|
||||||
"@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
"@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
||||||
|
@@ -2,6 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
|
@@ -71,6 +71,7 @@
|
|||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@types/react-i18next": "^8.1.0",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^3.0.8",
|
||||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import ThemeProvider from '@/components/ThemeProvider'
|
import ThemeProvider from '@/components/ThemeProvider'
|
||||||
|
import TabVisibilityProvider from '@/contexts/TabVisibilityProvider'
|
||||||
import MessageAlert from '@/components/MessageAlert'
|
import MessageAlert from '@/components/MessageAlert'
|
||||||
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
||||||
import StatusIndicator from '@/components/graph/StatusIndicator'
|
import StatusIndicator from '@/components/graph/StatusIndicator'
|
||||||
@@ -21,7 +22,7 @@ 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 currentTab = useSettingsStore.use.currentTab()
|
||||||
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
@@ -54,33 +55,35 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<main className="flex h-screen w-screen overflow-x-hidden">
|
<TabVisibilityProvider>
|
||||||
<Tabs
|
<main className="flex h-screen w-screen overflow-x-hidden">
|
||||||
defaultValue={currentTab}
|
<Tabs
|
||||||
className="!m-0 flex grow flex-col !p-0"
|
defaultValue={currentTab}
|
||||||
onValueChange={handleTabChange}
|
className="!m-0 flex grow flex-col !p-0"
|
||||||
>
|
onValueChange={handleTabChange}
|
||||||
<SiteHeader />
|
>
|
||||||
<div className="relative grow">
|
<SiteHeader />
|
||||||
<TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
|
<div className="relative grow">
|
||||||
<DocumentManager />
|
<TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
</TabsContent>
|
<DocumentManager />
|
||||||
<TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
|
</TabsContent>
|
||||||
<GraphViewer />
|
<TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
</TabsContent>
|
<GraphViewer />
|
||||||
<TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
|
</TabsContent>
|
||||||
<RetrievalTesting />
|
<TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
</TabsContent>
|
<RetrievalTesting />
|
||||||
<TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
|
</TabsContent>
|
||||||
<ApiSite />
|
<TabsContent value="api" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
</TabsContent>
|
<ApiSite />
|
||||||
</div>
|
</TabsContent>
|
||||||
</Tabs>
|
</div>
|
||||||
{enableHealthCheck && <StatusIndicator />}
|
</Tabs>
|
||||||
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
{enableHealthCheck && <StatusIndicator />}
|
||||||
{apiKeyInvalid && <ApiKeyAlert />}
|
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
||||||
<Toaster />
|
{apiKeyInvalid && <ApiKeyAlert />}
|
||||||
</main>
|
<Toaster />
|
||||||
|
</main>
|
||||||
|
</TabVisibilityProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
66
lightrag_webui/src/components/AppSettings.tsx
Normal file
66
lightrag_webui/src/components/AppSettings.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/Select'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { PaletteIcon } from 'lucide-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function AppSettings() {
|
||||||
|
const [opened, setOpened] = useState<boolean>(false)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const language = useSettingsStore.use.language()
|
||||||
|
const setLanguage = useSettingsStore.use.setLanguage()
|
||||||
|
|
||||||
|
const theme = useSettingsStore.use.theme()
|
||||||
|
const setTheme = useSettingsStore.use.setTheme()
|
||||||
|
|
||||||
|
const handleLanguageChange = useCallback((value: string) => {
|
||||||
|
setLanguage(value as 'en' | 'zh')
|
||||||
|
}, [setLanguage])
|
||||||
|
|
||||||
|
const handleThemeChange = useCallback((value: string) => {
|
||||||
|
setTheme(value as 'light' | 'dark' | 'system')
|
||||||
|
}, [setTheme])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={opened} onOpenChange={setOpened}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||||
|
<PaletteIcon className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="bottom" align="end" className="w-56">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">{t('settings.language')}</label>
|
||||||
|
<Select value={language} onValueChange={handleLanguageChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="en">English</SelectItem>
|
||||||
|
<SelectItem value="zh">中文</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">{t('settings.theme')}</label>
|
||||||
|
<Select value={theme} onValueChange={handleThemeChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="light">{t('settings.light')}</SelectItem>
|
||||||
|
<SelectItem value="dark">{t('settings.dark')}</SelectItem>
|
||||||
|
<SelectItem value="system">{t('settings.system')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
24
lightrag_webui/src/components/Root.tsx
Normal file
24
lightrag_webui/src/components/Root.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { StrictMode, useEffect, useState } from 'react'
|
||||||
|
import { initializeI18n } from '@/i18n'
|
||||||
|
import App from '@/App'
|
||||||
|
|
||||||
|
export const Root = () => {
|
||||||
|
const [isI18nInitialized, setIsI18nInitialized] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize i18n immediately with persisted language
|
||||||
|
initializeI18n().then(() => {
|
||||||
|
setIsI18nInitialized(true)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!isI18nInitialized) {
|
||||||
|
return null // or a loading spinner
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { createContext, useEffect, useState } from 'react'
|
import { createContext, useEffect } from 'react'
|
||||||
import { Theme, useSettingsStore } from '@/stores/settings'
|
import { Theme, useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
type ThemeProviderProps = {
|
type ThemeProviderProps = {
|
||||||
@@ -21,30 +21,32 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
|||||||
* Component that provides the theme state and setter function to its children.
|
* Component that provides the theme state and setter function to its children.
|
||||||
*/
|
*/
|
||||||
export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export default function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
const [theme, setTheme] = useState<Theme>(useSettingsStore.getState().theme)
|
const theme = useSettingsStore.use.theme()
|
||||||
|
const setTheme = useSettingsStore.use.setTheme()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = window.document.documentElement
|
const root = window.document.documentElement
|
||||||
root.classList.remove('light', 'dark')
|
root.classList.remove('light', 'dark')
|
||||||
|
|
||||||
if (theme === 'system') {
|
if (theme === 'system') {
|
||||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
? 'dark'
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
: 'light'
|
root.classList.remove('light', 'dark')
|
||||||
root.classList.add(systemTheme)
|
root.classList.add(e.matches ? 'dark' : 'light')
|
||||||
setTheme(systemTheme)
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.classList.add(theme)
|
root.classList.add(mediaQuery.matches ? 'dark' : 'light')
|
||||||
|
mediaQuery.addEventListener('change', handleChange)
|
||||||
|
|
||||||
|
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||||
|
} else {
|
||||||
|
root.classList.add(theme)
|
||||||
|
}
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
theme,
|
theme,
|
||||||
setTheme: (theme: Theme) => {
|
setTheme
|
||||||
useSettingsStore.getState().setTheme(theme)
|
|
||||||
setTheme(theme)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -13,15 +13,24 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
|
|||||||
* When the selected item changes, highlighted the node and center the camera on it.
|
* When the selected item changes, highlighted the node and center the camera on it.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!node) return
|
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
|
||||||
if (move) {
|
if (move) {
|
||||||
gotoNode(node)
|
if (node) {
|
||||||
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||||
|
gotoNode(node)
|
||||||
|
} else {
|
||||||
|
// If no node is selected but move is true, reset to default view
|
||||||
|
sigma.setCustomBBox(null)
|
||||||
|
sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 })
|
||||||
|
}
|
||||||
useGraphStore.getState().setMoveToSelectedNode(false)
|
useGraphStore.getState().setMoveToSelectedNode(false)
|
||||||
|
} else if (node) {
|
||||||
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
if (node) {
|
||||||
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [node, move, sigma, gotoNode])
|
}, [node, move, sigma, gotoNode])
|
||||||
|
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
||||||
|
import Graph from 'graphology'
|
||||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
||||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
||||||
import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||||
import useTheme from '@/hooks/useTheme'
|
import useTheme from '@/hooks/useTheme'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
|
|
||||||
@@ -21,7 +22,6 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
||||||
const { lightrageGraph } = useLightragGraph()
|
|
||||||
const sigma = useSigma<NodeType, EdgeType>()
|
const sigma = useSigma<NodeType, EdgeType>()
|
||||||
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
||||||
const setSettings = useSetSettings<NodeType, EdgeType>()
|
const setSettings = useSetSettings<NodeType, EdgeType>()
|
||||||
@@ -34,21 +34,25 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges()
|
||||||
|
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
||||||
|
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
||||||
|
const renderLabels = useSettingsStore.use.showNodeLabel()
|
||||||
const selectedNode = useGraphStore.use.selectedNode()
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
const selectedEdge = useGraphStore.use.selectedEdge()
|
const selectedEdge = useGraphStore.use.selectedEdge()
|
||||||
const focusedEdge = useGraphStore.use.focusedEdge()
|
const focusedEdge = useGraphStore.use.focusedEdge()
|
||||||
|
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount or maxIterations changes
|
* When component mount or maxIterations changes
|
||||||
* => load the graph and apply layout
|
* => load the graph and apply layout
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Create & load the graph
|
if (sigmaGraph) {
|
||||||
const graph = lightrageGraph()
|
loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>)
|
||||||
loadGraph(graph)
|
assignLayout()
|
||||||
assignLayout()
|
}
|
||||||
}, [assignLayout, loadGraph, lightrageGraph, maxIterations])
|
}, [assignLayout, loadGraph, sigmaGraph, maxIterations])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount
|
* When component mount
|
||||||
@@ -58,39 +62,52 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } =
|
||||||
useGraphStore.getState()
|
useGraphStore.getState()
|
||||||
|
|
||||||
// Register the events
|
// Define event types
|
||||||
registerEvents({
|
type NodeEvent = { node: string; event: { original: MouseEvent | TouchEvent } }
|
||||||
enterNode: (event) => {
|
type EdgeEvent = { edge: string; event: { original: MouseEvent | TouchEvent } }
|
||||||
|
|
||||||
|
// Register all events, but edge events will only be processed if enableEdgeEvents is true
|
||||||
|
const events: Record<string, any> = {
|
||||||
|
enterNode: (event: NodeEvent) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setFocusedNode(event.node)
|
setFocusedNode(event.node)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
leaveNode: (event) => {
|
leaveNode: (event: NodeEvent) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setFocusedNode(null)
|
setFocusedNode(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickNode: (event) => {
|
clickNode: (event: NodeEvent) => {
|
||||||
setSelectedNode(event.node)
|
setSelectedNode(event.node)
|
||||||
setSelectedEdge(null)
|
setSelectedEdge(null)
|
||||||
},
|
},
|
||||||
clickEdge: (event) => {
|
clickStage: () => clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add edge event handlers if enableEdgeEvents is true
|
||||||
|
if (enableEdgeEvents) {
|
||||||
|
events.clickEdge = (event: EdgeEvent) => {
|
||||||
setSelectedEdge(event.edge)
|
setSelectedEdge(event.edge)
|
||||||
setSelectedNode(null)
|
setSelectedNode(null)
|
||||||
},
|
}
|
||||||
enterEdge: (event) => {
|
|
||||||
|
events.enterEdge = (event: EdgeEvent) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setFocusedEdge(event.edge)
|
setFocusedEdge(event.edge)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
leaveEdge: (event) => {
|
|
||||||
|
events.leaveEdge = (event: EdgeEvent) => {
|
||||||
if (!isButtonPressed(event.event.original)) {
|
if (!isButtonPressed(event.event.original)) {
|
||||||
setFocusedEdge(null)
|
setFocusedEdge(null)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
clickStage: () => clearSelection()
|
}
|
||||||
})
|
|
||||||
}, [registerEvents])
|
// Register the events
|
||||||
|
registerEvents(events)
|
||||||
|
}, [registerEvents, enableEdgeEvents])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount or hovered node change
|
* When component mount or hovered node change
|
||||||
@@ -101,7 +118,14 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined
|
const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined
|
||||||
const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined
|
const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined
|
||||||
|
|
||||||
|
// Update all dynamic settings directly without recreating the sigma container
|
||||||
setSettings({
|
setSettings({
|
||||||
|
// Update display settings
|
||||||
|
enableEdgeEvents,
|
||||||
|
renderEdgeLabels,
|
||||||
|
renderLabels,
|
||||||
|
|
||||||
|
// Node reducer for node appearance
|
||||||
nodeReducer: (node, data) => {
|
nodeReducer: (node, data) => {
|
||||||
const graph = sigma.getGraph()
|
const graph = sigma.getGraph()
|
||||||
const newData: NodeType & {
|
const newData: NodeType & {
|
||||||
@@ -140,6 +164,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
}
|
}
|
||||||
return newData
|
return newData
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Edge reducer for edge appearance
|
||||||
edgeReducer: (edge, data) => {
|
edgeReducer: (edge, data) => {
|
||||||
const graph = sigma.getGraph()
|
const graph = sigma.getGraph()
|
||||||
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
||||||
@@ -181,7 +207,10 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
sigma,
|
sigma,
|
||||||
disableHoverEffect,
|
disableHoverEffect,
|
||||||
theme,
|
theme,
|
||||||
hideUnselectedEdges
|
hideUnselectedEdges,
|
||||||
|
enableEdgeEvents,
|
||||||
|
renderEdgeLabels,
|
||||||
|
renderLabels
|
||||||
])
|
])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
@@ -1,37 +1,48 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import { AsyncSelect } from '@/components/ui/AsyncSelect'
|
import { AsyncSelect } from '@/components/ui/AsyncSelect'
|
||||||
import { getGraphLabels } from '@/api/lightrag'
|
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useGraphStore } from '@/stores/graph'
|
import { useGraphStore } from '@/stores/graph'
|
||||||
import { labelListLimit } from '@/lib/constants'
|
import { labelListLimit } from '@/lib/constants'
|
||||||
import MiniSearch from 'minisearch'
|
import MiniSearch from 'minisearch'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const lastGraph: any = {
|
|
||||||
graph: null,
|
|
||||||
searchEngine: null,
|
|
||||||
labels: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const GraphLabels = () => {
|
const GraphLabels = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const label = useSettingsStore.use.queryLabel()
|
const label = useSettingsStore.use.queryLabel()
|
||||||
const graph = useGraphStore.use.sigmaGraph()
|
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
|
||||||
|
const labelsLoadedRef = useRef(false)
|
||||||
|
|
||||||
const getSearchEngine = useCallback(async () => {
|
// Track if a fetch is in progress to prevent multiple simultaneous fetches
|
||||||
if (lastGraph.graph == graph) {
|
const fetchInProgressRef = useRef(false)
|
||||||
return {
|
|
||||||
labels: lastGraph.labels,
|
// Fetch labels once on component mount, using global flag to prevent duplicates
|
||||||
searchEngine: lastGraph.searchEngine
|
useEffect(() => {
|
||||||
}
|
// Check if we've already attempted to fetch labels in this session
|
||||||
}
|
const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
|
||||||
const labels = ['*'].concat(await getGraphLabels())
|
|
||||||
|
// Only fetch if we haven't attempted in this session and no fetch is in progress
|
||||||
// Ensure query label exists
|
if (!labelsFetchAttempted && !fetchInProgressRef.current) {
|
||||||
if (!labels.includes(useSettingsStore.getState().queryLabel)) {
|
fetchInProgressRef.current = true
|
||||||
useSettingsStore.getState().setQueryLabel(labels[0])
|
// Set global flag to indicate we've attempted to fetch in this session
|
||||||
|
useGraphStore.getState().setLabelsFetchAttempted(true)
|
||||||
|
|
||||||
|
console.log('Fetching graph labels (once per session)...')
|
||||||
|
|
||||||
|
useGraphStore.getState().fetchAllDatabaseLabels()
|
||||||
|
.then(() => {
|
||||||
|
labelsLoadedRef.current = true
|
||||||
|
fetchInProgressRef.current = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to fetch labels:', error)
|
||||||
|
fetchInProgressRef.current = false
|
||||||
|
// Reset global flag to allow retry
|
||||||
|
useGraphStore.getState().setLabelsFetchAttempted(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}, []) // Empty dependency array ensures this only runs once on mount
|
||||||
|
|
||||||
|
const getSearchEngine = useCallback(() => {
|
||||||
// Create search engine
|
// Create search engine
|
||||||
const searchEngine = new MiniSearch({
|
const searchEngine = new MiniSearch({
|
||||||
idField: 'id',
|
idField: 'id',
|
||||||
@@ -46,41 +57,32 @@ const GraphLabels = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Add documents
|
// Add documents
|
||||||
const documents = labels.map((str, index) => ({ id: index, value: str }))
|
const documents = allDatabaseLabels.map((str, index) => ({ id: index, value: str }))
|
||||||
searchEngine.addAll(documents)
|
searchEngine.addAll(documents)
|
||||||
|
|
||||||
lastGraph.graph = graph
|
|
||||||
lastGraph.searchEngine = searchEngine
|
|
||||||
lastGraph.labels = labels
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels,
|
labels: allDatabaseLabels,
|
||||||
searchEngine
|
searchEngine
|
||||||
}
|
}
|
||||||
}, [graph])
|
}, [allDatabaseLabels])
|
||||||
|
|
||||||
const fetchData = useCallback(
|
const fetchData = useCallback(
|
||||||
async (query?: string): Promise<string[]> => {
|
async (query?: string): Promise<string[]> => {
|
||||||
const { labels, searchEngine } = await getSearchEngine()
|
const { labels, searchEngine } = getSearchEngine()
|
||||||
|
|
||||||
let result: string[] = labels
|
let result: string[] = labels
|
||||||
if (query) {
|
if (query) {
|
||||||
// Search labels
|
// Search labels
|
||||||
result = searchEngine.search(query).map((r) => labels[r.id])
|
result = searchEngine.search(query).map((r: { id: number }) => labels[r.id])
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.length <= labelListLimit
|
return result.length <= labelListLimit
|
||||||
? result
|
? result
|
||||||
: [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
|
: [...result.slice(0, labelListLimit), '...']
|
||||||
},
|
},
|
||||||
[getSearchEngine]
|
[getSearchEngine]
|
||||||
)
|
)
|
||||||
|
|
||||||
const setQueryLabel = useCallback((label: string) => {
|
|
||||||
if (label.startsWith('And ') && label.endsWith(' others')) return
|
|
||||||
useSettingsStore.getState().setQueryLabel(label)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncSelect<string>
|
<AsyncSelect<string>
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
@@ -94,8 +96,38 @@ const GraphLabels = () => {
|
|||||||
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
||||||
label={t('graphPanel.graphLabels.label')}
|
label={t('graphPanel.graphLabels.label')}
|
||||||
placeholder={t('graphPanel.graphLabels.placeholder')}
|
placeholder={t('graphPanel.graphLabels.placeholder')}
|
||||||
value={label !== null ? label : ''}
|
value={label !== null ? label : '*'}
|
||||||
onChange={setQueryLabel}
|
onChange={(newLabel) => {
|
||||||
|
const currentLabel = useSettingsStore.getState().queryLabel
|
||||||
|
|
||||||
|
// select the last item means query all
|
||||||
|
if (newLabel === '...') {
|
||||||
|
newLabel = '*'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the fetch attempted flag to force a new data fetch
|
||||||
|
useGraphStore.getState().setGraphDataFetchAttempted(false)
|
||||||
|
|
||||||
|
// Clear current graph data to ensure complete reload when label changes
|
||||||
|
if (newLabel !== currentLabel) {
|
||||||
|
const graphStore = useGraphStore.getState();
|
||||||
|
graphStore.clearSelection();
|
||||||
|
|
||||||
|
// Reset the graph state but preserve the instance
|
||||||
|
if (graphStore.sigmaGraph) {
|
||||||
|
const nodes = Array.from(graphStore.sigmaGraph.nodes());
|
||||||
|
nodes.forEach(node => graphStore.sigmaGraph?.dropNode(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLabel === currentLabel && newLabel !== '*') {
|
||||||
|
// reselect the same itme means qery all
|
||||||
|
useSettingsStore.getState().setQueryLabel('*')
|
||||||
|
} else {
|
||||||
|
useSettingsStore.getState().setQueryLabel(newLabel)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
clearable={false} // Prevent clearing value on reselect
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { FC, useCallback, useMemo } from 'react'
|
import { FC, useCallback, useEffect, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
EdgeById,
|
EdgeById,
|
||||||
NodeById,
|
NodeById,
|
||||||
@@ -28,6 +28,7 @@ function OptionComponent(item: OptionItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageId = '__message_item'
|
const messageId = '__message_item'
|
||||||
|
// Reset this cache when graph changes to ensure fresh search results
|
||||||
const lastGraph: any = {
|
const lastGraph: any = {
|
||||||
graph: null,
|
graph: null,
|
||||||
searchEngine: null
|
searchEngine: null
|
||||||
@@ -48,6 +49,15 @@ export const GraphSearchInput = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const graph = useGraphStore.use.sigmaGraph()
|
const graph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
|
// Force reset the cache when graph changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (graph) {
|
||||||
|
// Reset cache to ensure fresh search results with new graph data
|
||||||
|
lastGraph.graph = null;
|
||||||
|
lastGraph.searchEngine = null;
|
||||||
|
}
|
||||||
|
}, [graph]);
|
||||||
|
|
||||||
const searchEngine = useMemo(() => {
|
const searchEngine = useMemo(() => {
|
||||||
if (lastGraph.graph == graph) {
|
if (lastGraph.graph == graph) {
|
||||||
return lastGraph.searchEngine
|
return lastGraph.searchEngine
|
||||||
@@ -85,8 +95,19 @@ export const GraphSearchInput = ({
|
|||||||
const loadOptions = useCallback(
|
const loadOptions = useCallback(
|
||||||
async (query?: string): Promise<OptionItem[]> => {
|
async (query?: string): Promise<OptionItem[]> => {
|
||||||
if (onFocus) onFocus(null)
|
if (onFocus) onFocus(null)
|
||||||
if (!query || !searchEngine) return []
|
if (!graph || !searchEngine) return []
|
||||||
const result: OptionItem[] = searchEngine.search(query).map((r) => ({
|
|
||||||
|
// If no query, return first searchResultLimit nodes
|
||||||
|
if (!query) {
|
||||||
|
const nodeIds = graph.nodes().slice(0, searchResultLimit)
|
||||||
|
return nodeIds.map(id => ({
|
||||||
|
id,
|
||||||
|
type: 'nodes'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If has query, search nodes
|
||||||
|
const result: OptionItem[] = searchEngine.search(query).map((r: { id: string }) => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
type: 'nodes'
|
type: 'nodes'
|
||||||
}))
|
}))
|
||||||
@@ -103,7 +124,7 @@ export const GraphSearchInput = ({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[searchEngine, onFocus]
|
[graph, searchEngine, onFocus, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -132,14 +132,22 @@ const PropertyRow = ({
|
|||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const getPropertyNameTranslation = (name: string) => {
|
||||||
|
const translationKey = `graphPanel.propertiesView.node.propertyNames.${name}`
|
||||||
|
const translation = t(translationKey)
|
||||||
|
return translation === translationKey ? name : translation
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-primary/60 tracking-wide">{name}</label>:
|
<label className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</label>:
|
||||||
<Text
|
<Text
|
||||||
className="hover:bg-primary/20 rounded p-1 text-ellipsis"
|
className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis"
|
||||||
tooltipClassName="max-w-80"
|
tooltipClassName="max-w-80"
|
||||||
text={value}
|
text={value}
|
||||||
tooltip={tooltip || value}
|
tooltip={tooltip || (typeof value === 'string' ? value : JSON.stringify(value, null, 2))}
|
||||||
side="left"
|
side="left"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
@@ -174,7 +182,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|||||||
{node.relationships.length > 0 && (
|
{node.relationships.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
||||||
{t('graphPanel.propertiesView.node.relationships')}
|
{t('graphPanel.propertiesView.node.relationships')}
|
||||||
</label>
|
</label>
|
||||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||||
{node.relationships.map(({ type, id, label }) => {
|
{node.relationships.map(({ type, id, label }) => {
|
||||||
|
@@ -8,9 +8,10 @@ import Input from '@/components/ui/Input'
|
|||||||
import { controlButtonVariant } from '@/lib/constants'
|
import { controlButtonVariant } from '@/lib/constants'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
|
import { useGraphStore } from '@/stores/graph'
|
||||||
|
|
||||||
import { SettingsIcon } from 'lucide-react'
|
import { SettingsIcon, RefreshCwIcon } from 'lucide-react'
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays a checkbox with a label.
|
* Component that displays a checkbox with a label.
|
||||||
@@ -114,6 +115,7 @@ const LabeledNumberInput = ({
|
|||||||
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>('')
|
const [tempApiKey, setTempApiKey] = useState<string>('')
|
||||||
|
const refreshLayout = useGraphStore.use.refreshLayout()
|
||||||
|
|
||||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||||
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
||||||
@@ -208,116 +210,126 @@ export default function Settings() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={opened} onOpenChange={setOpened}>
|
<>
|
||||||
<PopoverTrigger asChild>
|
<Button
|
||||||
<Button variant={controlButtonVariant} tooltip={t("graphPanel.sideBar.settings.settings")} size="icon">
|
variant={controlButtonVariant}
|
||||||
<SettingsIcon />
|
tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
|
||||||
</Button>
|
size="icon"
|
||||||
</PopoverTrigger>
|
onClick={refreshLayout}
|
||||||
<PopoverContent
|
|
||||||
side="right"
|
|
||||||
align="start"
|
|
||||||
className="mb-2 p-2"
|
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<RefreshCwIcon />
|
||||||
<LabeledCheckBox
|
</Button>
|
||||||
checked={enableHealthCheck}
|
<Popover open={opened} onOpenChange={setOpened}>
|
||||||
onCheckedChange={setEnableHealthCheck}
|
<PopoverTrigger asChild>
|
||||||
label={t("graphPanel.sideBar.settings.healthCheck")}
|
<Button variant={controlButtonVariant} tooltip={t('graphPanel.sideBar.settings.settings')} size="icon">
|
||||||
/>
|
<SettingsIcon />
|
||||||
|
</Button>
|
||||||
<Separator />
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
<LabeledCheckBox
|
side="right"
|
||||||
checked={showPropertyPanel}
|
align="start"
|
||||||
onCheckedChange={setShowPropertyPanel}
|
className="mb-2 p-2"
|
||||||
label={t("graphPanel.sideBar.settings.showPropertyPanel")}
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
/>
|
>
|
||||||
<LabeledCheckBox
|
|
||||||
checked={showNodeSearchBar}
|
|
||||||
onCheckedChange={setShowNodeSearchBar}
|
|
||||||
label={t("graphPanel.sideBar.settings.showSearchBar")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<LabeledCheckBox
|
|
||||||
checked={showNodeLabel}
|
|
||||||
onCheckedChange={setShowNodeLabel}
|
|
||||||
label={t("graphPanel.sideBar.settings.showNodeLabel")}
|
|
||||||
/>
|
|
||||||
<LabeledCheckBox
|
|
||||||
checked={enableNodeDrag}
|
|
||||||
onCheckedChange={setEnableNodeDrag}
|
|
||||||
label={t("graphPanel.sideBar.settings.nodeDraggable")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<LabeledCheckBox
|
|
||||||
checked={showEdgeLabel}
|
|
||||||
onCheckedChange={setShowEdgeLabel}
|
|
||||||
label={t("graphPanel.sideBar.settings.showEdgeLabel")}
|
|
||||||
/>
|
|
||||||
<LabeledCheckBox
|
|
||||||
checked={enableHideUnselectedEdges}
|
|
||||||
onCheckedChange={setEnableHideUnselectedEdges}
|
|
||||||
label={t("graphPanel.sideBar.settings.hideUnselectedEdges")}
|
|
||||||
/>
|
|
||||||
<LabeledCheckBox
|
|
||||||
checked={enableEdgeEvents}
|
|
||||||
onCheckedChange={setEnableEdgeEvents}
|
|
||||||
label={t("graphPanel.sideBar.settings.edgeEvents")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
<LabeledNumberInput
|
|
||||||
label={t("graphPanel.sideBar.settings.maxQueryDepth")}
|
|
||||||
min={1}
|
|
||||||
value={graphQueryMaxDepth}
|
|
||||||
onEditFinished={setGraphQueryMaxDepth}
|
|
||||||
/>
|
|
||||||
<LabeledNumberInput
|
|
||||||
label={t("graphPanel.sideBar.settings.minDegree")}
|
|
||||||
min={0}
|
|
||||||
value={graphMinDegree}
|
|
||||||
onEditFinished={setGraphMinDegree}
|
|
||||||
/>
|
|
||||||
<LabeledNumberInput
|
|
||||||
label={t("graphPanel.sideBar.settings.maxLayoutIterations")}
|
|
||||||
min={1}
|
|
||||||
max={20}
|
|
||||||
value={graphLayoutMaxIterations}
|
|
||||||
onEditFinished={setGraphLayoutMaxIterations}
|
|
||||||
/>
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-sm font-medium">{t("graphPanel.sideBar.settings.apiKey")}</label>
|
<LabeledCheckBox
|
||||||
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
|
checked={enableHealthCheck}
|
||||||
<div className="w-0 flex-1">
|
onCheckedChange={setEnableHealthCheck}
|
||||||
<Input
|
label={t('graphPanel.sideBar.settings.healthCheck')}
|
||||||
type="password"
|
/>
|
||||||
value={tempApiKey}
|
|
||||||
onChange={handleTempApiKeyChange}
|
<Separator />
|
||||||
placeholder={t("graphPanel.sideBar.settings.enterYourAPIkey")}
|
|
||||||
className="max-h-full w-full min-w-0"
|
<LabeledCheckBox
|
||||||
autoComplete="off"
|
checked={showPropertyPanel}
|
||||||
/>
|
onCheckedChange={setShowPropertyPanel}
|
||||||
</div>
|
label={t('graphPanel.sideBar.settings.showPropertyPanel')}
|
||||||
<Button
|
/>
|
||||||
onClick={setApiKey}
|
<LabeledCheckBox
|
||||||
variant="outline"
|
checked={showNodeSearchBar}
|
||||||
size="sm"
|
onCheckedChange={setShowNodeSearchBar}
|
||||||
className="max-h-full shrink-0"
|
label={t('graphPanel.sideBar.settings.showSearchBar')}
|
||||||
>
|
/>
|
||||||
{t("graphPanel.sideBar.settings.save")}
|
|
||||||
</Button>
|
<Separator />
|
||||||
</form>
|
|
||||||
|
<LabeledCheckBox
|
||||||
|
checked={showNodeLabel}
|
||||||
|
onCheckedChange={setShowNodeLabel}
|
||||||
|
label={t('graphPanel.sideBar.settings.showNodeLabel')}
|
||||||
|
/>
|
||||||
|
<LabeledCheckBox
|
||||||
|
checked={enableNodeDrag}
|
||||||
|
onCheckedChange={setEnableNodeDrag}
|
||||||
|
label={t('graphPanel.sideBar.settings.nodeDraggable')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<LabeledCheckBox
|
||||||
|
checked={showEdgeLabel}
|
||||||
|
onCheckedChange={setShowEdgeLabel}
|
||||||
|
label={t('graphPanel.sideBar.settings.showEdgeLabel')}
|
||||||
|
/>
|
||||||
|
<LabeledCheckBox
|
||||||
|
checked={enableHideUnselectedEdges}
|
||||||
|
onCheckedChange={setEnableHideUnselectedEdges}
|
||||||
|
label={t('graphPanel.sideBar.settings.hideUnselectedEdges')}
|
||||||
|
/>
|
||||||
|
<LabeledCheckBox
|
||||||
|
checked={enableEdgeEvents}
|
||||||
|
onCheckedChange={setEnableEdgeEvents}
|
||||||
|
label={t('graphPanel.sideBar.settings.edgeEvents')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
<LabeledNumberInput
|
||||||
|
label={t('graphPanel.sideBar.settings.maxQueryDepth')}
|
||||||
|
min={1}
|
||||||
|
value={graphQueryMaxDepth}
|
||||||
|
onEditFinished={setGraphQueryMaxDepth}
|
||||||
|
/>
|
||||||
|
<LabeledNumberInput
|
||||||
|
label={t('graphPanel.sideBar.settings.minDegree')}
|
||||||
|
min={0}
|
||||||
|
value={graphMinDegree}
|
||||||
|
onEditFinished={setGraphMinDegree}
|
||||||
|
/>
|
||||||
|
<LabeledNumberInput
|
||||||
|
label={t('graphPanel.sideBar.settings.maxLayoutIterations')}
|
||||||
|
min={1}
|
||||||
|
max={30}
|
||||||
|
value={graphLayoutMaxIterations}
|
||||||
|
onEditFinished={setGraphLayoutMaxIterations}
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">{t('graphPanel.sideBar.settings.apiKey')}</label>
|
||||||
|
<form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<div className="w-0 flex-1">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={tempApiKey}
|
||||||
|
onChange={handleTempApiKeyChange}
|
||||||
|
placeholder={t('graphPanel.sideBar.settings.enterYourAPIkey')}
|
||||||
|
className="max-h-full w-full min-w-0"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={setApiKey}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="max-h-full shrink-0"
|
||||||
|
>
|
||||||
|
{t('graphPanel.sideBar.settings.save')}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopoverContent>
|
||||||
</PopoverContent>
|
</Popover>
|
||||||
</Popover>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
21
lightrag_webui/src/components/graph/SettingsDisplay.tsx
Normal file
21
lightrag_webui/src/components/graph/SettingsDisplay.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays current values of important graph settings
|
||||||
|
* Positioned to the right of the toolbar at the bottom-left corner
|
||||||
|
*/
|
||||||
|
const SettingsDisplay = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const graphQueryMaxDepth = useSettingsStore.use.graphQueryMaxDepth()
|
||||||
|
const graphMinDegree = useSettingsStore.use.graphMinDegree()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute bottom-2 left-[calc(2rem+2.5rem)] flex items-center gap-2 text-xs text-gray-400">
|
||||||
|
<div>{t('graphPanel.sideBar.settings.depth')}: {graphQueryMaxDepth}</div>
|
||||||
|
<div>{t('graphPanel.sideBar.settings.degree')}: {graphMinDegree}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsDisplay
|
@@ -25,7 +25,7 @@ export default function QuerySettings() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex shrink-0 flex-col">
|
<Card className="flex shrink-0 flex-col min-w-[180px]">
|
||||||
<CardHeader className="px-4 pt-4 pb-2">
|
<CardHeader className="px-4 pt-4 pb-2">
|
||||||
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
||||||
<CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
<CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { useDebounce } from '@/hooks/useDebounce'
|
import { useDebounce } from '@/hooks/useDebounce'
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CommandList hidden={!open || debouncedSearchTerm.length === 0}>
|
<CommandList hidden={!open}>
|
||||||
{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 &&
|
||||||
@@ -204,7 +204,7 @@ export function AsyncSearch<T>({
|
|||||||
))}
|
))}
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{options.map((option, idx) => (
|
{options.map((option, idx) => (
|
||||||
<>
|
<React.Fragment key={getOptionValue(option) + `-fragment-${idx}`}>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={getOptionValue(option) + `${idx}`}
|
key={getOptionValue(option) + `${idx}`}
|
||||||
value={getOptionValue(option)}
|
value={getOptionValue(option)}
|
||||||
@@ -215,9 +215,9 @@ export function AsyncSearch<T>({
|
|||||||
{renderOption(option)}
|
{renderOption(option)}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
{idx !== options.length - 1 && (
|
{idx !== options.length - 1 && (
|
||||||
<div key={idx} className="bg-foreground/10 h-[1px]" />
|
<div key={`divider-${idx}`} className="bg-foreground/10 h-[1px]" />
|
||||||
)}
|
)}
|
||||||
</>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
|
37
lightrag_webui/src/components/ui/TabContent.tsx
Normal file
37
lightrag_webui/src/components/ui/TabContent.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility';
|
||||||
|
|
||||||
|
interface TabContentProps {
|
||||||
|
tabId: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TabContent component that manages visibility based on tab selection
|
||||||
|
* Works with the TabVisibilityContext to show/hide content based on active tab
|
||||||
|
*/
|
||||||
|
const TabContent: React.FC<TabContentProps> = ({ tabId, children, className = '' }) => {
|
||||||
|
const { isTabVisible, setTabVisibility } = useTabVisibility();
|
||||||
|
const isVisible = isTabVisible(tabId);
|
||||||
|
|
||||||
|
// Register this tab with the context when mounted
|
||||||
|
useEffect(() => {
|
||||||
|
setTabVisibility(tabId, true);
|
||||||
|
|
||||||
|
// Cleanup when unmounted
|
||||||
|
return () => {
|
||||||
|
setTabVisibility(tabId, false);
|
||||||
|
};
|
||||||
|
}, [tabId, setTabVisibility]);
|
||||||
|
|
||||||
|
// Use CSS to hide content instead of not rendering it
|
||||||
|
// This prevents components from unmounting when tabs are switched
|
||||||
|
return (
|
||||||
|
<div className={`${className} ${isVisible ? '' : 'hidden'}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabContent;
|
@@ -42,9 +42,13 @@ const TabsContent = React.forwardRef<
|
|||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
'ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
|
||||||
|
'data-[state=inactive]:invisible data-[state=active]:visible',
|
||||||
|
'h-full w-full',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
// Force mounting of inactive tabs to preserve WebGL contexts
|
||||||
|
forceMount
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@@ -10,30 +10,43 @@ const TooltipTrigger = TooltipPrimitive.Trigger
|
|||||||
|
|
||||||
const processTooltipContent = (content: string) => {
|
const processTooltipContent = (content: string) => {
|
||||||
if (typeof content !== 'string') return content
|
if (typeof content !== 'string') return content
|
||||||
return content.split('\\n').map((line, i) => (
|
return (
|
||||||
<React.Fragment key={i}>
|
<div className="relative top-0 pt-1 whitespace-pre-wrap break-words">
|
||||||
{line}
|
{content}
|
||||||
{i < content.split('\\n').length - 1 && <br />}
|
</div>
|
||||||
</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, children, ...props }, ref) => (
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
||||||
<TooltipPrimitive.Content
|
align?: 'start' | 'center' | 'end'
|
||||||
ref={ref}
|
}
|
||||||
sideOffset={sideOffset}
|
>(({ className, side = 'left', align = 'start', children, ...props }, ref) => {
|
||||||
className={cn(
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||||
'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
|
React.useEffect(() => {
|
||||||
)}
|
if (contentRef.current) {
|
||||||
{...props}
|
contentRef.current.scrollTop = 0;
|
||||||
>
|
}
|
||||||
{typeof children === 'string' ? processTooltipContent(children) : children}
|
}, [children]);
|
||||||
</TooltipPrimitive.Content>
|
|
||||||
))
|
return (
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
side={side}
|
||||||
|
align={align}
|
||||||
|
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 max-h-[60vh] overflow-y-auto whitespace-pre-wrap break-words rounded-md border px-3 py-2 text-sm shadow-md',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{typeof children === 'string' ? processTooltipContent(children) : children}
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
);
|
||||||
|
})
|
||||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
|
53
lightrag_webui/src/contexts/TabVisibilityProvider.tsx
Normal file
53
lightrag_webui/src/contexts/TabVisibilityProvider.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { TabVisibilityContext } from './context';
|
||||||
|
import { TabVisibilityContextType } from './types';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
|
||||||
|
interface TabVisibilityProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider component for the TabVisibility context
|
||||||
|
* Manages the visibility state of tabs throughout the application
|
||||||
|
*/
|
||||||
|
export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
|
||||||
|
// Get current tab from settings store
|
||||||
|
const currentTab = useSettingsStore.use.currentTab();
|
||||||
|
|
||||||
|
// Initialize visibility state with current tab as visible
|
||||||
|
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
|
||||||
|
[currentTab]: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update visibility when current tab changes
|
||||||
|
useEffect(() => {
|
||||||
|
setVisibleTabs((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[currentTab]: true
|
||||||
|
}));
|
||||||
|
}, [currentTab]);
|
||||||
|
|
||||||
|
// Create the context value with memoization to prevent unnecessary re-renders
|
||||||
|
const contextValue = useMemo<TabVisibilityContextType>(
|
||||||
|
() => ({
|
||||||
|
visibleTabs,
|
||||||
|
setTabVisibility: (tabId: string, isVisible: boolean) => {
|
||||||
|
setVisibleTabs((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tabId]: isVisible,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
isTabVisible: (tabId: string) => !!visibleTabs[tabId],
|
||||||
|
}),
|
||||||
|
[visibleTabs]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabVisibilityContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</TabVisibilityContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TabVisibilityProvider;
|
12
lightrag_webui/src/contexts/context.ts
Normal file
12
lightrag_webui/src/contexts/context.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { TabVisibilityContextType } from './types';
|
||||||
|
|
||||||
|
// Default context value
|
||||||
|
const defaultContext: TabVisibilityContextType = {
|
||||||
|
visibleTabs: {},
|
||||||
|
setTabVisibility: () => {},
|
||||||
|
isTabVisible: () => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the context
|
||||||
|
export const TabVisibilityContext = createContext<TabVisibilityContextType>(defaultContext);
|
5
lightrag_webui/src/contexts/types.ts
Normal file
5
lightrag_webui/src/contexts/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface TabVisibilityContextType {
|
||||||
|
visibleTabs: Record<string, boolean>;
|
||||||
|
setTabVisibility: (tabId: string, isVisible: boolean) => void;
|
||||||
|
isTabVisible: (tabId: string) => boolean;
|
||||||
|
}
|
17
lightrag_webui/src/contexts/useTabVisibility.ts
Normal file
17
lightrag_webui/src/contexts/useTabVisibility.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { TabVisibilityContext } from './context';
|
||||||
|
import { TabVisibilityContextType } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to access the tab visibility context
|
||||||
|
* @returns The tab visibility context
|
||||||
|
*/
|
||||||
|
export const useTabVisibility = (): TabVisibilityContextType => {
|
||||||
|
const context = useContext(TabVisibilityContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTabVisibility must be used within a TabVisibilityProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
@@ -1,5 +1,40 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
import { backendBaseUrl } from '@/lib/constants'
|
import { backendBaseUrl } from '@/lib/constants'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function ApiSite() {
|
export default function ApiSite() {
|
||||||
return <iframe src={backendBaseUrl + '/docs'} className="size-full" />
|
const { t } = useTranslation()
|
||||||
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isApiTabVisible = isTabVisible('api')
|
||||||
|
const [iframeLoaded, setIframeLoaded] = useState(false)
|
||||||
|
|
||||||
|
// Load the iframe once on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (!iframeLoaded) {
|
||||||
|
setIframeLoaded(true)
|
||||||
|
}
|
||||||
|
}, [iframeLoaded])
|
||||||
|
|
||||||
|
// Use CSS to hide content when tab is not visible
|
||||||
|
return (
|
||||||
|
<div className={`size-full ${isApiTabVisible ? '' : 'hidden'}`}>
|
||||||
|
{iframeLoaded ? (
|
||||||
|
<iframe
|
||||||
|
src={backendBaseUrl + '/docs'}
|
||||||
|
className="size-full w-full h-full"
|
||||||
|
style={{ width: '100%', height: '100%', border: 'none' }}
|
||||||
|
// Use key to ensure iframe doesn't reload
|
||||||
|
key="api-docs-iframe"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-background">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||||
|
<p>{t('apiSite.loading')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -26,6 +27,9 @@ export default function DocumentManager() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||||
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isDocumentsTabVisible = isTabVisible('documents')
|
||||||
|
const initialLoadRef = useRef(false)
|
||||||
|
|
||||||
const fetchDocuments = useCallback(async () => {
|
const fetchDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -48,11 +52,15 @@ export default function DocumentManager() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
|
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, [setDocs])
|
}, [setDocs, t])
|
||||||
|
|
||||||
|
// Only fetch documents when the tab becomes visible for the first time
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDocuments()
|
if (isDocumentsTabVisible && !initialLoadRef.current) {
|
||||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
fetchDocuments()
|
||||||
|
initialLoadRef.current = true
|
||||||
|
}
|
||||||
|
}, [isDocumentsTabVisible, fetchDocuments])
|
||||||
|
|
||||||
const scanDocuments = useCallback(async () => {
|
const scanDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -61,21 +69,24 @@ export default function DocumentManager() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
|
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, [])
|
}, [t])
|
||||||
|
|
||||||
|
// Only set up polling when the tab is visible and health is good
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isDocumentsTabVisible || !health) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
if (!health) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await fetchDocuments()
|
await fetchDocuments()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [health, fetchDocuments])
|
}, [health, fetchDocuments, t, isDocumentsTabVisible])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="!size-full !rounded-none !border-none">
|
<Card className="!size-full !rounded-none !border-none">
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
// import { MiniMap } from '@react-sigma/minimap'
|
// import { MiniMap } from '@react-sigma/minimap'
|
||||||
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
||||||
import { Settings as SigmaSettings } from 'sigma/settings'
|
import { Settings as SigmaSettings } from 'sigma/settings'
|
||||||
@@ -17,6 +18,7 @@ import Settings from '@/components/graph/Settings'
|
|||||||
import GraphSearch from '@/components/graph/GraphSearch'
|
import GraphSearch from '@/components/graph/GraphSearch'
|
||||||
import GraphLabels from '@/components/graph/GraphLabels'
|
import GraphLabels from '@/components/graph/GraphLabels'
|
||||||
import PropertiesView from '@/components/graph/PropertiesView'
|
import PropertiesView from '@/components/graph/PropertiesView'
|
||||||
|
import SettingsDisplay from '@/components/graph/SettingsDisplay'
|
||||||
|
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { useGraphStore } from '@/stores/graph'
|
import { useGraphStore } from '@/stores/graph'
|
||||||
@@ -90,8 +92,12 @@ const GraphEvents = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Disable the autoscale at the first down interaction
|
// Disable the autoscale at the first down interaction
|
||||||
mousedown: () => {
|
mousedown: (e) => {
|
||||||
if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox())
|
// Only set custom BBox if it's a drag operation (mouse button is pressed)
|
||||||
|
const mouseEvent = e.original as MouseEvent;
|
||||||
|
if (mouseEvent.buttons !== 0 && !sigma.getCustomBBox()) {
|
||||||
|
sigma.setCustomBBox(sigma.getBBox())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [registerEvents, sigma, draggedNode])
|
}, [registerEvents, sigma, draggedNode])
|
||||||
@@ -101,27 +107,46 @@ const GraphEvents = () => {
|
|||||||
|
|
||||||
const GraphViewer = () => {
|
const GraphViewer = () => {
|
||||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||||
|
const sigmaRef = useRef<any>(null)
|
||||||
|
const initAttemptedRef = useRef(false)
|
||||||
|
|
||||||
const selectedNode = useGraphStore.use.selectedNode()
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
||||||
|
const isFetching = useGraphStore.use.isFetching()
|
||||||
|
const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
|
||||||
|
|
||||||
|
// Get tab visibility
|
||||||
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
||||||
|
|
||||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||||
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
||||||
const renderLabels = useSettingsStore.use.showNodeLabel()
|
|
||||||
|
|
||||||
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
|
||||||
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
||||||
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
|
||||||
|
|
||||||
|
// Handle component mount/unmount and tab visibility
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSigmaSettings({
|
// When component mounts or tab becomes visible
|
||||||
...defaultSigmaSettings,
|
if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
|
||||||
enableEdgeEvents,
|
// If tab is visible but graph is not rendering, try to enable rendering
|
||||||
renderEdgeLabels,
|
useGraphStore.getState().setShouldRender(true)
|
||||||
renderLabels
|
initAttemptedRef.current = true
|
||||||
})
|
console.log('Graph viewer initialized')
|
||||||
}, [renderLabels, enableEdgeEvents, renderEdgeLabels])
|
}
|
||||||
|
|
||||||
|
// Cleanup function when component unmounts
|
||||||
|
return () => {
|
||||||
|
// Only log cleanup, don't actually clean up the WebGL context
|
||||||
|
// This allows the WebGL context to persist across tab switches
|
||||||
|
console.log('Graph viewer cleanup')
|
||||||
|
}
|
||||||
|
}, [isGraphTabVisible, shouldRender, isFetching])
|
||||||
|
|
||||||
|
// Initialize sigma settings once on component mount
|
||||||
|
// All dynamic settings will be updated in GraphControl using useSetSettings
|
||||||
|
useEffect(() => {
|
||||||
|
setSigmaSettings(defaultSigmaSettings)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
||||||
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
||||||
@@ -142,43 +167,73 @@ const GraphViewer = () => {
|
|||||||
[selectedNode]
|
[selectedNode]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Since TabsContent now forces mounting of all tabs, we need to conditionally render
|
||||||
|
// the SigmaContainer based on visibility to avoid unnecessary rendering
|
||||||
return (
|
return (
|
||||||
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
|
<div className="relative h-full w-full">
|
||||||
<GraphControl />
|
{/* Only render the SigmaContainer when the tab is visible */}
|
||||||
|
{isGraphTabVisible ? (
|
||||||
|
<SigmaContainer
|
||||||
|
settings={sigmaSettings}
|
||||||
|
className="!bg-background !size-full overflow-hidden"
|
||||||
|
ref={sigmaRef}
|
||||||
|
>
|
||||||
|
<GraphControl />
|
||||||
|
|
||||||
{enableNodeDrag && <GraphEvents />}
|
{enableNodeDrag && <GraphEvents />}
|
||||||
|
|
||||||
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
||||||
|
|
||||||
<div className="absolute top-2 left-2 flex items-start gap-2">
|
<div className="absolute top-2 left-2 flex items-start gap-2">
|
||||||
<GraphLabels />
|
<GraphLabels />
|
||||||
{showNodeSearchBar && (
|
{showNodeSearchBar && (
|
||||||
<GraphSearch
|
<GraphSearch
|
||||||
value={searchInitSelectedNode}
|
value={searchInitSelectedNode}
|
||||||
onFocus={onSearchFocus}
|
onFocus={onSearchFocus}
|
||||||
onChange={onSearchSelect}
|
onChange={onSearchSelect}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
|
<div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
|
||||||
<Settings />
|
<Settings />
|
||||||
<ZoomControl />
|
<ZoomControl />
|
||||||
<LayoutsControl />
|
<LayoutsControl />
|
||||||
<FullScreenControl />
|
<FullScreenControl />
|
||||||
{/* <ThemeToggle /> */}
|
{/* <ThemeToggle /> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showPropertyPanel && (
|
{showPropertyPanel && (
|
||||||
<div className="absolute top-2 right-2">
|
<div className="absolute top-2 right-2">
|
||||||
<PropertiesView />
|
<PropertiesView />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
|
||||||
|
<MiniMap width="100px" height="100px" />
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<SettingsDisplay />
|
||||||
|
</SigmaContainer>
|
||||||
|
) : (
|
||||||
|
// Placeholder when tab is not visible
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
{/* Placeholder content */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
|
{/* Loading overlay - shown when data is loading */}
|
||||||
<MiniMap width="100px" height="100px" />
|
{isFetching && (
|
||||||
</div> */}
|
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
||||||
</SigmaContainer>
|
<div className="text-center">
|
||||||
|
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||||
|
<p>Loading Graph Data...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import { SiteInfo } from '@/lib/constants'
|
import { SiteInfo } from '@/lib/constants'
|
||||||
import ThemeToggle from '@/components/ThemeToggle'
|
import AppSettings from '@/components/AppSettings'
|
||||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -67,12 +67,14 @@ export default function SiteHeader() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex items-center">
|
<nav className="flex items-center">
|
||||||
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
|
<div className="flex items-center gap-2">
|
||||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
|
||||||
<GithubIcon className="size-4" aria-hidden="true" />
|
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||||
</a>
|
<GithubIcon className="size-4" aria-hidden="true" />
|
||||||
</Button>
|
</a>
|
||||||
<ThemeToggle />
|
</Button>
|
||||||
|
<AppSettings />
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import Graph, { DirectedGraph } from 'graphology'
|
import Graph, { DirectedGraph } from 'graphology'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
import { randomColor, errorMessage } from '@/lib/utils'
|
import { randomColor, errorMessage } from '@/lib/utils'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
import { useGraphStore, RawGraph } from '@/stores/graph'
|
import { useGraphStore, RawGraph } from '@/stores/graph'
|
||||||
import { queryGraphs } from '@/api/lightrag'
|
import { queryGraphs } from '@/api/lightrag'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
|
|
||||||
import seedrandom from 'seedrandom'
|
import seedrandom from 'seedrandom'
|
||||||
|
|
||||||
@@ -136,15 +137,23 @@ const fetchGraph = async (label: string, maxDepth: number, minDegree: number) =>
|
|||||||
return rawGraph
|
return rawGraph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new graph instance with the raw graph data
|
||||||
const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
||||||
|
// Always create a new graph instance
|
||||||
const graph = new DirectedGraph()
|
const graph = new DirectedGraph()
|
||||||
|
|
||||||
|
// Add nodes from raw graph data
|
||||||
for (const rawNode of rawGraph?.nodes ?? []) {
|
for (const rawNode of rawGraph?.nodes ?? []) {
|
||||||
|
// Ensure we have fresh random positions for nodes
|
||||||
|
seedrandom(rawNode.id + Date.now().toString(), { global: true })
|
||||||
|
const x = Math.random()
|
||||||
|
const y = Math.random()
|
||||||
|
|
||||||
graph.addNode(rawNode.id, {
|
graph.addNode(rawNode.id, {
|
||||||
label: rawNode.labels.join(', '),
|
label: rawNode.labels.join(', '),
|
||||||
color: rawNode.color,
|
color: rawNode.color,
|
||||||
x: rawNode.x,
|
x: x,
|
||||||
y: rawNode.y,
|
y: y,
|
||||||
size: rawNode.size,
|
size: rawNode.size,
|
||||||
// for node-border
|
// for node-border
|
||||||
borderColor: Constants.nodeBorderColor,
|
borderColor: Constants.nodeBorderColor,
|
||||||
@@ -152,6 +161,7 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add edges from raw graph data
|
||||||
for (const rawEdge of rawGraph?.edges ?? []) {
|
for (const rawEdge of rawGraph?.edges ?? []) {
|
||||||
rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
|
rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
|
||||||
label: rawEdge.type || undefined
|
label: rawEdge.type || undefined
|
||||||
@@ -161,14 +171,30 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastQueryLabel = { label: '', maxQueryDepth: 0, minDegree: 0 }
|
|
||||||
|
|
||||||
const useLightrangeGraph = () => {
|
const useLightrangeGraph = () => {
|
||||||
const queryLabel = useSettingsStore.use.queryLabel()
|
const queryLabel = useSettingsStore.use.queryLabel()
|
||||||
const rawGraph = useGraphStore.use.rawGraph()
|
const rawGraph = useGraphStore.use.rawGraph()
|
||||||
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||||
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
||||||
const minDegree = useSettingsStore.use.graphMinDegree()
|
const minDegree = useSettingsStore.use.graphMinDegree()
|
||||||
|
const isFetching = useGraphStore.use.isFetching()
|
||||||
|
|
||||||
|
// Get tab visibility
|
||||||
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
||||||
|
|
||||||
|
// Track previous parameters to detect actual changes
|
||||||
|
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
|
||||||
|
|
||||||
|
// Use ref to track if data has been loaded and initial load
|
||||||
|
const dataLoadedRef = useRef(false)
|
||||||
|
const initialLoadRef = useRef(false)
|
||||||
|
|
||||||
|
// Check if parameters have changed
|
||||||
|
const paramsChanged =
|
||||||
|
prevParamsRef.current.queryLabel !== queryLabel ||
|
||||||
|
prevParamsRef.current.maxQueryDepth !== maxQueryDepth ||
|
||||||
|
prevParamsRef.current.minDegree !== minDegree
|
||||||
|
|
||||||
const getNode = useCallback(
|
const getNode = useCallback(
|
||||||
(nodeId: string) => {
|
(nodeId: string) => {
|
||||||
@@ -184,35 +210,131 @@ const useLightrangeGraph = () => {
|
|||||||
[rawGraph]
|
[rawGraph]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
// Track if a fetch is in progress to prevent multiple simultaneous fetches
|
||||||
if (queryLabel) {
|
const fetchInProgressRef = useRef(false)
|
||||||
if (lastQueryLabel.label !== queryLabel ||
|
|
||||||
lastQueryLabel.maxQueryDepth !== maxQueryDepth ||
|
|
||||||
lastQueryLabel.minDegree !== minDegree) {
|
|
||||||
lastQueryLabel.label = queryLabel
|
|
||||||
lastQueryLabel.maxQueryDepth = maxQueryDepth
|
|
||||||
lastQueryLabel.minDegree = minDegree
|
|
||||||
|
|
||||||
|
// Data fetching logic - simplified but preserving TAB visibility check
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip if fetch is already in progress
|
||||||
|
if (fetchInProgressRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no query label, reset the graph
|
||||||
|
if (!queryLabel) {
|
||||||
|
if (rawGraph !== null || sigmaGraph !== null) {
|
||||||
const state = useGraphStore.getState()
|
const state = useGraphStore.getState()
|
||||||
state.reset()
|
state.reset()
|
||||||
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
|
state.setGraphDataFetchAttempted(false)
|
||||||
// console.debug('Query label: ' + queryLabel)
|
state.setLabelsFetchAttempted(false)
|
||||||
state.setSigmaGraph(createSigmaGraph(data))
|
}
|
||||||
data?.buildDynamicMap()
|
dataLoadedRef.current = false
|
||||||
state.setRawGraph(data)
|
initialLoadRef.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if parameters have changed
|
||||||
|
if (!isFetching && !fetchInProgressRef.current &&
|
||||||
|
(paramsChanged || !useGraphStore.getState().graphDataFetchAttempted)) {
|
||||||
|
|
||||||
|
// Only fetch data if the Graph tab is visible
|
||||||
|
if (!isGraphTabVisible) {
|
||||||
|
console.log('Graph tab not visible, skipping data fetch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set flags
|
||||||
|
fetchInProgressRef.current = true
|
||||||
|
useGraphStore.getState().setGraphDataFetchAttempted(true)
|
||||||
|
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
state.setIsFetching(true)
|
||||||
|
state.setShouldRender(false) // Disable rendering during data loading
|
||||||
|
|
||||||
|
// Clear selection and highlighted nodes before fetching new graph
|
||||||
|
state.clearSelection()
|
||||||
|
if (state.sigmaGraph) {
|
||||||
|
state.sigmaGraph.forEachNode((node) => {
|
||||||
|
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const state = useGraphStore.getState()
|
// Update parameter reference
|
||||||
state.reset()
|
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree }
|
||||||
state.setSigmaGraph(new DirectedGraph())
|
|
||||||
|
console.log('Fetching graph data...')
|
||||||
|
|
||||||
|
// Use a local copy of the parameters
|
||||||
|
const currentQueryLabel = queryLabel
|
||||||
|
const currentMaxQueryDepth = maxQueryDepth
|
||||||
|
const currentMinDegree = minDegree
|
||||||
|
|
||||||
|
// Fetch graph data
|
||||||
|
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
state.reset()
|
||||||
|
|
||||||
|
// Create and set new graph directly
|
||||||
|
const newSigmaGraph = createSigmaGraph(data)
|
||||||
|
data?.buildDynamicMap()
|
||||||
|
|
||||||
|
// Set new graph data
|
||||||
|
state.setSigmaGraph(newSigmaGraph)
|
||||||
|
state.setRawGraph(data)
|
||||||
|
|
||||||
|
// No longer need to extract labels from graph data
|
||||||
|
|
||||||
|
// Update flags
|
||||||
|
dataLoadedRef.current = true
|
||||||
|
initialLoadRef.current = true
|
||||||
|
fetchInProgressRef.current = false
|
||||||
|
|
||||||
|
// Reset camera view
|
||||||
|
state.setMoveToSelectedNode(true)
|
||||||
|
|
||||||
|
// Enable rendering if the tab is visible
|
||||||
|
state.setShouldRender(isGraphTabVisible)
|
||||||
|
state.setIsFetching(false)
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Error fetching graph data:', error)
|
||||||
|
|
||||||
|
// Reset state on error
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
state.setIsFetching(false)
|
||||||
|
state.setShouldRender(isGraphTabVisible)
|
||||||
|
dataLoadedRef.current = false
|
||||||
|
fetchInProgressRef.current = false
|
||||||
|
state.setGraphDataFetchAttempted(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [queryLabel, maxQueryDepth, minDegree])
|
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph])
|
||||||
|
|
||||||
|
// Update rendering state and handle tab visibility changes
|
||||||
|
useEffect(() => {
|
||||||
|
// When tab becomes visible
|
||||||
|
if (isGraphTabVisible) {
|
||||||
|
// If we have data, enable rendering
|
||||||
|
if (rawGraph) {
|
||||||
|
useGraphStore.getState().setShouldRender(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We no longer reset the fetch attempted flag here to prevent continuous API calls
|
||||||
|
} else {
|
||||||
|
// When tab becomes invisible, disable rendering
|
||||||
|
useGraphStore.getState().setShouldRender(false)
|
||||||
|
}
|
||||||
|
}, [isGraphTabVisible, rawGraph])
|
||||||
|
|
||||||
const lightrageGraph = useCallback(() => {
|
const lightrageGraph = useCallback(() => {
|
||||||
|
// If we already have a graph instance, return it
|
||||||
if (sigmaGraph) {
|
if (sigmaGraph) {
|
||||||
return sigmaGraph as Graph<NodeType, EdgeType>
|
return sigmaGraph as Graph<NodeType, EdgeType>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no graph exists yet, create a new one and store it
|
||||||
|
console.log('Creating new Sigma graph instance')
|
||||||
const graph = new DirectedGraph()
|
const graph = new DirectedGraph()
|
||||||
useGraphStore.getState().setSigmaGraph(graph)
|
useGraphStore.getState().setSigmaGraph(graph)
|
||||||
return graph as Graph<NodeType, EdgeType>
|
return graph as Graph<NodeType, EdgeType>
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
import i18n from "i18next";
|
|
||||||
import { initReactI18next } from "react-i18next";
|
|
||||||
|
|
||||||
import en from "./locales/en.json";
|
|
||||||
import zh from "./locales/zh.json";
|
|
||||||
|
|
||||||
i18n
|
|
||||||
.use(initReactI18next)
|
|
||||||
.init({
|
|
||||||
resources: {
|
|
||||||
en: { translation: en },
|
|
||||||
zh: { translation: zh }
|
|
||||||
},
|
|
||||||
lng: "en", // default
|
|
||||||
fallbackLng: "en",
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default i18n;
|
|
37
lightrag_webui/src/i18n.ts
Normal file
37
lightrag_webui/src/i18n.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import i18n from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
|
||||||
|
import en from './locales/en.json'
|
||||||
|
import zh from './locales/zh.json'
|
||||||
|
|
||||||
|
// Function to sync i18n with store state
|
||||||
|
export const initializeI18n = async (): Promise<typeof i18n> => {
|
||||||
|
// Get initial language from store
|
||||||
|
const initialLanguage = useSettingsStore.getState().language
|
||||||
|
|
||||||
|
// Initialize with store language
|
||||||
|
await i18n.use(initReactI18next).init({
|
||||||
|
resources: {
|
||||||
|
en: { translation: en },
|
||||||
|
zh: { translation: zh }
|
||||||
|
},
|
||||||
|
lng: initialLanguage,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Subscribe to language changes
|
||||||
|
useSettingsStore.subscribe((state) => {
|
||||||
|
const currentLanguage = state.language
|
||||||
|
if (i18n.language !== currentLanguage) {
|
||||||
|
i18n.changeLanguage(currentLanguage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return i18n
|
||||||
|
}
|
||||||
|
|
||||||
|
export default i18n
|
@@ -15,8 +15,8 @@ export const edgeColorDarkTheme = '#969696'
|
|||||||
export const edgeColorSelected = '#F57F17'
|
export const edgeColorSelected = '#F57F17'
|
||||||
export const edgeColorHighlighted = '#B2EBF2'
|
export const edgeColorHighlighted = '#B2EBF2'
|
||||||
|
|
||||||
export const searchResultLimit = 20
|
export const searchResultLimit = 50
|
||||||
export const labelListLimit = 40
|
export const labelListLimit = 100
|
||||||
|
|
||||||
export const minNodeSize = 4
|
export const minNodeSize = 4
|
||||||
export const maxNodeSize = 20
|
export const maxNodeSize = 20
|
||||||
|
@@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"settings": {
|
||||||
|
"language": "Language",
|
||||||
|
"theme": "Theme",
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark",
|
||||||
|
"system": "System"
|
||||||
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"documents": "Documents",
|
"documents": "Documents",
|
||||||
"knowledgeGraph": "Knowledge Graph",
|
"knowledgeGraph": "Knowledge Graph",
|
||||||
@@ -79,9 +86,12 @@
|
|||||||
"maxQueryDepth": "Max Query Depth",
|
"maxQueryDepth": "Max Query Depth",
|
||||||
"minDegree": "Minimum Degree",
|
"minDegree": "Minimum Degree",
|
||||||
"maxLayoutIterations": "Max Layout Iterations",
|
"maxLayoutIterations": "Max Layout Iterations",
|
||||||
|
"depth": "Depth",
|
||||||
|
"degree": "Degree",
|
||||||
"apiKey": "API Key",
|
"apiKey": "API Key",
|
||||||
"enterYourAPIkey": "Enter your API key",
|
"enterYourAPIkey": "Enter your API key",
|
||||||
"save": "Save"
|
"save": "Save",
|
||||||
|
"refreshLayout": "Refresh Layout"
|
||||||
},
|
},
|
||||||
|
|
||||||
"zoomControl": {
|
"zoomControl": {
|
||||||
@@ -140,7 +150,14 @@
|
|||||||
"labels": "Labels",
|
"labels": "Labels",
|
||||||
"degree": "Degree",
|
"degree": "Degree",
|
||||||
"properties": "Properties",
|
"properties": "Properties",
|
||||||
"relationships": "Relationships"
|
"relationships": "Relationships",
|
||||||
|
"propertyNames": {
|
||||||
|
"description": "Description",
|
||||||
|
"entity_id": "Name",
|
||||||
|
"entity_type": "Type",
|
||||||
|
"source_id": "SrcID",
|
||||||
|
"Neighbour": "Neigh"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"edge": {
|
"edge": {
|
||||||
"title": "Relationship",
|
"title": "Relationship",
|
||||||
@@ -230,5 +247,8 @@
|
|||||||
"streamResponse": "Stream Response",
|
"streamResponse": "Stream Response",
|
||||||
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"apiSite": {
|
||||||
|
"loading": "Loading API Documentation..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"settings": {
|
||||||
|
"language": "语言",
|
||||||
|
"theme": "主题",
|
||||||
|
"light": "浅色",
|
||||||
|
"dark": "深色",
|
||||||
|
"system": "系统"
|
||||||
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"documents": "文档",
|
"documents": "文档",
|
||||||
"knowledgeGraph": "知识图谱",
|
"knowledgeGraph": "知识图谱",
|
||||||
@@ -6,41 +13,41 @@
|
|||||||
"api": "API",
|
"api": "API",
|
||||||
"projectRepository": "项目仓库",
|
"projectRepository": "项目仓库",
|
||||||
"themeToggle": {
|
"themeToggle": {
|
||||||
"switchToLight": "切换到亮色主题",
|
"switchToLight": "切换到浅色主题",
|
||||||
"switchToDark": "切换到暗色主题"
|
"switchToDark": "切换到深色主题"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documentPanel": {
|
"documentPanel": {
|
||||||
"clearDocuments": {
|
"clearDocuments": {
|
||||||
"button": "清除",
|
"button": "清空",
|
||||||
"tooltip": "清除文档",
|
"tooltip": "清空文档",
|
||||||
"title": "清除文档",
|
"title": "清空文档",
|
||||||
"confirm": "您确定要清除所有文档吗?",
|
"confirm": "确定要清空所有文档吗?",
|
||||||
"confirmButton": "确定",
|
"confirmButton": "确定",
|
||||||
"success": "文档已成功清除",
|
"success": "文档清空成功",
|
||||||
"failed": "清除文档失败:\n{{message}}",
|
"failed": "清空文档失败:\n{{message}}",
|
||||||
"error": "清除文档失败:\n{{error}}"
|
"error": "清空文档失败:\n{{error}}"
|
||||||
},
|
},
|
||||||
"uploadDocuments": {
|
"uploadDocuments": {
|
||||||
"button": "上传",
|
"button": "上传",
|
||||||
"tooltip": "上传文档",
|
"tooltip": "上传文档",
|
||||||
"title": "上传文档",
|
"title": "上传文档",
|
||||||
"description": "拖放文档到此处或点击浏览。",
|
"description": "拖拽文件到此处或点击浏览",
|
||||||
"uploading": "正在上传 {{name}}: {{percent}}%",
|
"uploading": "正在上传 {{name}}:{{percent}}%",
|
||||||
"success": "上传成功:\n{{name}} 上传成功",
|
"success": "上传成功:\n{{name}} 上传完成",
|
||||||
"failed": "上传失败:\n{{name}}\n{{message}}",
|
"failed": "上传失败:\n{{name}}\n{{message}}",
|
||||||
"error": "上传失败:\n{{name}}\n{{error}}",
|
"error": "上传失败:\n{{name}}\n{{error}}",
|
||||||
"generalError": "上传失败\n{{error}}",
|
"generalError": "上传失败\n{{error}}",
|
||||||
"fileTypes": "支持的文件类型: TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
"fileTypes": "支持的文件类型:TXT, MD, DOCX, PDF, PPTX, RTF, ODT, EPUB, HTML, HTM, TEX, JSON, XML, YAML, YML, CSV, LOG, CONF, INI, PROPERTIES, SQL, BAT, SH, C, CPP, PY, JAVA, JS, TS, SWIFT, GO, RB, PHP, CSS, SCSS, LESS"
|
||||||
},
|
},
|
||||||
"documentManager": {
|
"documentManager": {
|
||||||
"title": "文档管理",
|
"title": "文档管理",
|
||||||
"scanButton": "扫描",
|
"scanButton": "扫描",
|
||||||
"scanTooltip": "扫描文档",
|
"scanTooltip": "扫描文档",
|
||||||
"uploadedTitle": "已上传文档",
|
"uploadedTitle": "已上传文档",
|
||||||
"uploadedDescription": "已上传文档及其状态列表。",
|
"uploadedDescription": "已上传文档列表及其状态",
|
||||||
"emptyTitle": "暂无文档",
|
"emptyTitle": "无文档",
|
||||||
"emptyDescription": "尚未上传任何文档。",
|
"emptyDescription": "还没有上传任何文档",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"summary": "摘要",
|
"summary": "摘要",
|
||||||
@@ -54,7 +61,7 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"processing": "处理中",
|
"processing": "处理中",
|
||||||
"pending": "待处理",
|
"pending": "等待中",
|
||||||
"failed": "失败"
|
"failed": "失败"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -74,39 +81,39 @@
|
|||||||
"showNodeLabel": "显示节点标签",
|
"showNodeLabel": "显示节点标签",
|
||||||
"nodeDraggable": "节点可拖动",
|
"nodeDraggable": "节点可拖动",
|
||||||
"showEdgeLabel": "显示边标签",
|
"showEdgeLabel": "显示边标签",
|
||||||
"hideUnselectedEdges": "隐藏未选中边",
|
"hideUnselectedEdges": "隐藏未选中的边",
|
||||||
"edgeEvents": "边事件",
|
"edgeEvents": "边事件",
|
||||||
"maxQueryDepth": "最大查询深度",
|
"maxQueryDepth": "最大查询深度",
|
||||||
"minDegree": "最小度数",
|
"minDegree": "最小度数",
|
||||||
"maxLayoutIterations": "最大布局迭代次数",
|
"maxLayoutIterations": "最大布局迭代次数",
|
||||||
"apiKey": "API 密钥",
|
"depth": "深度",
|
||||||
"enterYourAPIkey": "输入您的 API 密钥",
|
"degree": "邻边",
|
||||||
"save": "保存"
|
"apiKey": "API密钥",
|
||||||
|
"enterYourAPIkey": "输入您的API密钥",
|
||||||
|
"save": "保存",
|
||||||
|
"refreshLayout": "刷新布局"
|
||||||
},
|
},
|
||||||
|
|
||||||
"zoomControl": {
|
"zoomControl": {
|
||||||
"zoomIn": "放大",
|
"zoomIn": "放大",
|
||||||
"zoomOut": "缩小",
|
"zoomOut": "缩小",
|
||||||
"resetZoom": "重置缩放"
|
"resetZoom": "重置缩放"
|
||||||
},
|
},
|
||||||
|
|
||||||
"layoutsControl": {
|
"layoutsControl": {
|
||||||
"startAnimation": "开始布局动画",
|
"startAnimation": "开始布局动画",
|
||||||
"stopAnimation": "停止布局动画",
|
"stopAnimation": "停止布局动画",
|
||||||
"layoutGraph": "布局图",
|
"layoutGraph": "图布局",
|
||||||
"layouts": {
|
"layouts": {
|
||||||
"Circular": "环形布局",
|
"Circular": "环形",
|
||||||
"Circlepack": "圆形打包布局",
|
"Circlepack": "圆形打包",
|
||||||
"Random": "随机布局",
|
"Random": "随机",
|
||||||
"Noverlaps": "无重叠布局",
|
"Noverlaps": "无重叠",
|
||||||
"Force Directed": "力导向布局",
|
"Force Directed": "力导向",
|
||||||
"Force Atlas": "力导向图谱布局"
|
"Force Atlas": "力图"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"fullScreenControl": {
|
"fullScreenControl": {
|
||||||
"fullScreen": "全屏",
|
"fullScreen": "全屏",
|
||||||
"windowed": "窗口模式"
|
"windowed": "窗口"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"statusIndicator": {
|
"statusIndicator": {
|
||||||
@@ -118,17 +125,17 @@
|
|||||||
"storageInfo": "存储信息",
|
"storageInfo": "存储信息",
|
||||||
"workingDirectory": "工作目录",
|
"workingDirectory": "工作目录",
|
||||||
"inputDirectory": "输入目录",
|
"inputDirectory": "输入目录",
|
||||||
"llmConfig": "LLM 配置",
|
"llmConfig": "LLM配置",
|
||||||
"llmBinding": "LLM 绑定",
|
"llmBinding": "LLM绑定",
|
||||||
"llmBindingHost": "LLM 绑定主机",
|
"llmBindingHost": "LLM绑定主机",
|
||||||
"llmModel": "LLM 模型",
|
"llmModel": "LLM模型",
|
||||||
"maxTokens": "最大 Token 数",
|
"maxTokens": "最大令牌数",
|
||||||
"embeddingConfig": "嵌入配置",
|
"embeddingConfig": "嵌入配置",
|
||||||
"embeddingBinding": "嵌入绑定",
|
"embeddingBinding": "嵌入绑定",
|
||||||
"embeddingBindingHost": "嵌入绑定主机",
|
"embeddingBindingHost": "嵌入绑定主机",
|
||||||
"embeddingModel": "嵌入模型",
|
"embeddingModel": "嵌入模型",
|
||||||
"storageConfig": "存储配置",
|
"storageConfig": "存储配置",
|
||||||
"kvStorage": "KV 存储",
|
"kvStorage": "KV存储",
|
||||||
"docStatusStorage": "文档状态存储",
|
"docStatusStorage": "文档状态存储",
|
||||||
"graphStorage": "图存储",
|
"graphStorage": "图存储",
|
||||||
"vectorStorage": "向量存储"
|
"vectorStorage": "向量存储"
|
||||||
@@ -140,96 +147,93 @@
|
|||||||
"labels": "标签",
|
"labels": "标签",
|
||||||
"degree": "度数",
|
"degree": "度数",
|
||||||
"properties": "属性",
|
"properties": "属性",
|
||||||
"relationships": "关系"
|
"relationships": "关系",
|
||||||
|
"propertyNames": {
|
||||||
|
"description": "描述",
|
||||||
|
"entity_id": "名称",
|
||||||
|
"entity_type": "类型",
|
||||||
|
"source_id": "信源ID",
|
||||||
|
"Neighbour": "邻接"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"edge": {
|
"edge": {
|
||||||
"title": "关系",
|
"title": "关系",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"source": "源",
|
"source": "源节点",
|
||||||
"target": "目标",
|
"target": "目标节点",
|
||||||
"properties": "属性"
|
"properties": "属性"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜索节点...",
|
"placeholder": "搜索节点...",
|
||||||
"message": "以及其它 {count} 项"
|
"message": "还有 {count} 个"
|
||||||
},
|
},
|
||||||
"graphLabels": {
|
"graphLabels": {
|
||||||
"selectTooltip": "选择查询标签",
|
"selectTooltip": "选择查询标签",
|
||||||
"noLabels": "未找到标签",
|
"noLabels": "未找到标签",
|
||||||
"label": "标签",
|
"label": "标签",
|
||||||
"placeholder": "搜索标签...",
|
"placeholder": "搜索标签...",
|
||||||
"andOthers": "以及其它 {count} 个"
|
"andOthers": "还有 {count} 个"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"retrievePanel": {
|
"retrievePanel": {
|
||||||
"chatMessage": {
|
"chatMessage": {
|
||||||
"copyTooltip": "复制到剪贴板",
|
"copyTooltip": "复制到剪贴板",
|
||||||
"copyError": "无法复制文本到剪贴板"
|
"copyError": "复制文本到剪贴板失败"
|
||||||
},
|
},
|
||||||
|
|
||||||
"retrieval": {
|
"retrieval": {
|
||||||
"startPrompt": "在下面输入您的查询以开始检索",
|
"startPrompt": "输入查询开始检索",
|
||||||
"clear": "清除",
|
"clear": "清空",
|
||||||
"send": "发送",
|
"send": "发送",
|
||||||
"placeholder": "输入您的查询...",
|
"placeholder": "输入查询...",
|
||||||
"error": "错误:无法获取响应"
|
"error": "错误:获取响应失败"
|
||||||
},
|
},
|
||||||
"querySettings": {
|
"querySettings": {
|
||||||
"parametersTitle": "参数设置",
|
"parametersTitle": "参数",
|
||||||
"parametersDescription": "配置查询参数",
|
"parametersDescription": "配置查询参数",
|
||||||
|
|
||||||
"queryMode": "查询模式",
|
"queryMode": "查询模式",
|
||||||
"queryModeTooltip": "选择检索策略:\n• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索",
|
"queryModeTooltip": "选择检索策略:\n• Naive:基础搜索,无高级技术\n• Local:上下文相关信息检索\n• Global:利用全局知识库\n• Hybrid:结合本地和全局检索\n• Mix:整合知识图谱和向量检索",
|
||||||
"queryModeOptions": {
|
"queryModeOptions": {
|
||||||
"naive": "朴素",
|
"naive": "朴素",
|
||||||
"local": "本地",
|
"local": "本地",
|
||||||
"global": "全局",
|
"global": "全局",
|
||||||
"hybrid": "混合",
|
"hybrid": "混合",
|
||||||
"mix": "综合"
|
"mix": "混合"
|
||||||
},
|
},
|
||||||
|
|
||||||
"responseFormat": "响应格式",
|
"responseFormat": "响应格式",
|
||||||
"responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号",
|
"responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点",
|
||||||
"responseFormatOptions": {
|
"responseFormatOptions": {
|
||||||
"multipleParagraphs": "多个段落",
|
"multipleParagraphs": "多段落",
|
||||||
"singleParagraph": "单个段落",
|
"singleParagraph": "单段落",
|
||||||
"bulletPoints": "项目符号"
|
"bulletPoints": "要点"
|
||||||
},
|
},
|
||||||
|
"topK": "Top K结果",
|
||||||
"topK": "Top K 结果数",
|
"topKTooltip": "检索的顶部项目数。在'local'模式下表示实体,在'global'模式下表示关系",
|
||||||
"topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系",
|
"topKPlaceholder": "结果数量",
|
||||||
"topKPlaceholder": "结果数",
|
"maxTokensTextUnit": "文本单元最大令牌数",
|
||||||
|
"maxTokensTextUnitTooltip": "每个检索文本块允许的最大令牌数",
|
||||||
"maxTokensTextUnit": "文本单元最大 Token 数",
|
"maxTokensGlobalContext": "全局上下文最大令牌数",
|
||||||
"maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数",
|
"maxTokensGlobalContextTooltip": "全局检索中关系描述的最大令牌数",
|
||||||
|
"maxTokensLocalContext": "本地上下文最大令牌数",
|
||||||
"maxTokensGlobalContext": "全局上下文最大 Token 数",
|
"maxTokensLocalContextTooltip": "本地检索中实体描述的最大令牌数",
|
||||||
"maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
|
|
||||||
|
|
||||||
"maxTokensLocalContext": "本地上下文最大 Token 数",
|
|
||||||
"maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
|
|
||||||
|
|
||||||
"historyTurns": "历史轮次",
|
"historyTurns": "历史轮次",
|
||||||
"historyTurnsTooltip": "在响应上下文中考虑的完整对话轮次(用户-助手对)",
|
"historyTurnsTooltip": "响应上下文中考虑的完整对话轮次(用户-助手对)数量",
|
||||||
"historyTurnsPlaceholder": "历史轮次的数量",
|
"historyTurnsPlaceholder": "历史轮次数",
|
||||||
|
|
||||||
"hlKeywords": "高级关键词",
|
"hlKeywords": "高级关键词",
|
||||||
"hlKeywordsTooltip": "检索时优先考虑的高级关键词。请用逗号分隔",
|
"hlKeywordsTooltip": "检索中优先考虑的高级关键词列表。用逗号分隔",
|
||||||
"hlkeywordsPlaceHolder": "输入关键词",
|
"hlkeywordsPlaceHolder": "输入关键词",
|
||||||
|
|
||||||
"llKeywords": "低级关键词",
|
"llKeywords": "低级关键词",
|
||||||
"llKeywordsTooltip": "用于优化检索焦点的低级关键词。请用逗号分隔",
|
"llKeywordsTooltip": "用于细化检索重点的低级关键词列表。用逗号分隔",
|
||||||
|
"onlyNeedContext": "仅需上下文",
|
||||||
"onlyNeedContext": "仅需要上下文",
|
"onlyNeedContextTooltip": "如果为True,仅返回检索到的上下文而不生成响应",
|
||||||
"onlyNeedContextTooltip": "如果为 True,则仅返回检索到的上下文,而不会生成回复",
|
"onlyNeedPrompt": "仅需提示",
|
||||||
|
"onlyNeedPromptTooltip": "如果为True,仅返回生成的提示而不产生响应",
|
||||||
"onlyNeedPrompt": "仅需要提示",
|
|
||||||
"onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复",
|
|
||||||
|
|
||||||
"streamResponse": "流式响应",
|
"streamResponse": "流式响应",
|
||||||
"streamResponseTooltip": "如果为 True,则启用流式输出以获得实时响应"
|
"streamResponseTooltip": "如果为True,启用实时流式输出响应"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"apiSite": {
|
||||||
|
"loading": "正在加载 API 文档..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,5 @@
|
|||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import { Root } from '@/components/Root'
|
||||||
import "./i18n";
|
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(<Root />)
|
||||||
createRoot(document.getElementById('root')!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>
|
|
||||||
)
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { createSelectors } from '@/lib/utils'
|
import { createSelectors } from '@/lib/utils'
|
||||||
import { DirectedGraph } from 'graphology'
|
import { DirectedGraph } from 'graphology'
|
||||||
|
import { getGraphLabels } from '@/api/lightrag'
|
||||||
|
|
||||||
export type RawNodeType = {
|
export type RawNodeType = {
|
||||||
id: string
|
id: string
|
||||||
@@ -65,9 +66,17 @@ interface GraphState {
|
|||||||
|
|
||||||
rawGraph: RawGraph | null
|
rawGraph: RawGraph | null
|
||||||
sigmaGraph: DirectedGraph | null
|
sigmaGraph: DirectedGraph | null
|
||||||
|
allDatabaseLabels: string[]
|
||||||
|
|
||||||
moveToSelectedNode: boolean
|
moveToSelectedNode: boolean
|
||||||
|
isFetching: boolean
|
||||||
|
shouldRender: boolean
|
||||||
|
|
||||||
|
// Global flags to track data fetching attempts
|
||||||
|
graphDataFetchAttempted: boolean
|
||||||
|
labelsFetchAttempted: boolean
|
||||||
|
|
||||||
|
refreshLayout: () => void
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
||||||
setFocusedNode: (nodeId: string | null) => void
|
setFocusedNode: (nodeId: string | null) => void
|
||||||
setSelectedEdge: (edgeId: string | null) => void
|
setSelectedEdge: (edgeId: string | null) => void
|
||||||
@@ -79,19 +88,47 @@ interface GraphState {
|
|||||||
|
|
||||||
setRawGraph: (rawGraph: RawGraph | null) => void
|
setRawGraph: (rawGraph: RawGraph | null) => void
|
||||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
||||||
|
setAllDatabaseLabels: (labels: string[]) => void
|
||||||
|
fetchAllDatabaseLabels: () => Promise<void>
|
||||||
|
setIsFetching: (isFetching: boolean) => void
|
||||||
|
setShouldRender: (shouldRender: boolean) => void
|
||||||
|
|
||||||
|
// Methods to set global flags
|
||||||
|
setGraphDataFetchAttempted: (attempted: boolean) => void
|
||||||
|
setLabelsFetchAttempted: (attempted: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useGraphStoreBase = create<GraphState>()((set) => ({
|
const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
||||||
selectedNode: null,
|
selectedNode: null,
|
||||||
focusedNode: null,
|
focusedNode: null,
|
||||||
selectedEdge: null,
|
selectedEdge: null,
|
||||||
focusedEdge: null,
|
focusedEdge: null,
|
||||||
|
|
||||||
moveToSelectedNode: false,
|
moveToSelectedNode: false,
|
||||||
|
isFetching: false,
|
||||||
|
shouldRender: false,
|
||||||
|
|
||||||
|
// Initialize global flags
|
||||||
|
graphDataFetchAttempted: false,
|
||||||
|
labelsFetchAttempted: false,
|
||||||
|
|
||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
sigmaGraph: null,
|
||||||
|
allDatabaseLabels: ['*'],
|
||||||
|
|
||||||
|
refreshLayout: () => {
|
||||||
|
const currentGraph = get().sigmaGraph;
|
||||||
|
if (currentGraph) {
|
||||||
|
get().clearSelection();
|
||||||
|
get().setSigmaGraph(null);
|
||||||
|
setTimeout(() => {
|
||||||
|
get().setSigmaGraph(currentGraph);
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||||
|
setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||||
set({ selectedNode: nodeId, moveToSelectedNode }),
|
set({ selectedNode: nodeId, moveToSelectedNode }),
|
||||||
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
||||||
@@ -104,25 +141,58 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
selectedEdge: null,
|
selectedEdge: null,
|
||||||
focusedEdge: null
|
focusedEdge: null
|
||||||
}),
|
}),
|
||||||
reset: () =>
|
reset: () => {
|
||||||
|
// Get the existing graph
|
||||||
|
const existingGraph = get().sigmaGraph;
|
||||||
|
|
||||||
|
// If we have an existing graph, clear it by removing all nodes
|
||||||
|
if (existingGraph) {
|
||||||
|
const nodes = Array.from(existingGraph.nodes());
|
||||||
|
nodes.forEach(node => existingGraph.dropNode(node));
|
||||||
|
}
|
||||||
|
|
||||||
set({
|
set({
|
||||||
selectedNode: null,
|
selectedNode: null,
|
||||||
focusedNode: null,
|
focusedNode: null,
|
||||||
selectedEdge: null,
|
selectedEdge: null,
|
||||||
focusedEdge: null,
|
focusedEdge: null,
|
||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
// Keep the existing graph instance but with cleared data
|
||||||
moveToSelectedNode: false
|
moveToSelectedNode: false,
|
||||||
}),
|
shouldRender: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setRawGraph: (rawGraph: RawGraph | null) =>
|
setRawGraph: (rawGraph: RawGraph | null) =>
|
||||||
set({
|
set({
|
||||||
rawGraph
|
rawGraph
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => set({ sigmaGraph }),
|
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => {
|
||||||
|
// Replace graph instance, no need to keep WebGL context
|
||||||
|
set({ sigmaGraph });
|
||||||
|
},
|
||||||
|
|
||||||
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
|
setAllDatabaseLabels: (labels: string[]) => set({ allDatabaseLabels: labels }),
|
||||||
|
|
||||||
|
fetchAllDatabaseLabels: async () => {
|
||||||
|
try {
|
||||||
|
console.log('Fetching all database labels...');
|
||||||
|
const labels = await getGraphLabels();
|
||||||
|
set({ allDatabaseLabels: ['*', ...labels] });
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch all database labels:', error);
|
||||||
|
set({ allDatabaseLabels: ['*'] });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
|
||||||
|
|
||||||
|
// Methods to set global flags
|
||||||
|
setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
|
||||||
|
setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const useGraphStore = createSelectors(useGraphStoreBase)
|
const useGraphStore = createSelectors(useGraphStoreBase)
|
||||||
|
@@ -5,6 +5,7 @@ import { defaultQueryLabel } from '@/lib/constants'
|
|||||||
import { Message, QueryRequest } from '@/api/lightrag'
|
import { Message, QueryRequest } from '@/api/lightrag'
|
||||||
|
|
||||||
type Theme = 'dark' | 'light' | 'system'
|
type Theme = 'dark' | 'light' | 'system'
|
||||||
|
type Language = 'en' | 'zh'
|
||||||
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
@@ -46,6 +47,9 @@ interface SettingsState {
|
|||||||
theme: Theme
|
theme: Theme
|
||||||
setTheme: (theme: Theme) => void
|
setTheme: (theme: Theme) => void
|
||||||
|
|
||||||
|
language: Language
|
||||||
|
setLanguage: (lang: Language) => void
|
||||||
|
|
||||||
enableHealthCheck: boolean
|
enableHealthCheck: boolean
|
||||||
setEnableHealthCheck: (enable: boolean) => void
|
setEnableHealthCheck: (enable: boolean) => void
|
||||||
|
|
||||||
@@ -57,7 +61,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
|
language: 'en',
|
||||||
showPropertyPanel: true,
|
showPropertyPanel: true,
|
||||||
showNodeSearchBar: true,
|
showNodeSearchBar: true,
|
||||||
|
|
||||||
@@ -70,7 +74,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
|
|
||||||
graphQueryMaxDepth: 3,
|
graphQueryMaxDepth: 3,
|
||||||
graphMinDegree: 0,
|
graphMinDegree: 0,
|
||||||
graphLayoutMaxIterations: 10,
|
graphLayoutMaxIterations: 15,
|
||||||
|
|
||||||
queryLabel: defaultQueryLabel,
|
queryLabel: defaultQueryLabel,
|
||||||
|
|
||||||
@@ -99,6 +103,16 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
|
|
||||||
setTheme: (theme: Theme) => set({ theme }),
|
setTheme: (theme: Theme) => set({ theme }),
|
||||||
|
|
||||||
|
setLanguage: (language: Language) => {
|
||||||
|
set({ language })
|
||||||
|
// Update i18n after state is updated
|
||||||
|
import('i18next').then(({ default: i18n }) => {
|
||||||
|
if (i18n.language !== language) {
|
||||||
|
i18n.changeLanguage(language)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
setGraphLayoutMaxIterations: (iterations: number) =>
|
setGraphLayoutMaxIterations: (iterations: number) =>
|
||||||
set({
|
set({
|
||||||
graphLayoutMaxIterations: iterations
|
graphLayoutMaxIterations: iterations
|
||||||
@@ -129,7 +143,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 7,
|
version: 8,
|
||||||
migrate: (state: any, version: number) => {
|
migrate: (state: any, version: number) => {
|
||||||
if (version < 2) {
|
if (version < 2) {
|
||||||
state.showEdgeLabel = false
|
state.showEdgeLabel = false
|
||||||
@@ -166,7 +180,11 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
}
|
}
|
||||||
if (version < 7) {
|
if (version < 7) {
|
||||||
state.graphQueryMaxDepth = 3
|
state.graphQueryMaxDepth = 3
|
||||||
state.graphLayoutMaxIterations = 10
|
state.graphLayoutMaxIterations = 15
|
||||||
|
}
|
||||||
|
if (version < 8) {
|
||||||
|
state.graphMinDegree = 0
|
||||||
|
state.language = 'en'
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,9 @@ export default defineConfig({
|
|||||||
target: import.meta.env.VITE_BACKEND_URL || 'http://localhost:9621',
|
target: import.meta.env.VITE_BACKEND_URL || 'http://localhost:9621',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: endpoint === '/api' ?
|
rewrite: endpoint === '/api' ?
|
||||||
(path) => path.replace(/^\/api/, '') : undefined
|
(path) => path.replace(/^\/api/, '') :
|
||||||
|
endpoint === '/docs' || endpoint === '/openapi.json' ?
|
||||||
|
(path) => path : undefined
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
) : {}
|
) : {}
|
||||||
|
@@ -4,6 +4,12 @@ future
|
|||||||
|
|
||||||
# Basic modules
|
# Basic modules
|
||||||
gensim
|
gensim
|
||||||
|
|
||||||
|
# Additional Packages for export Functionality
|
||||||
|
pandas>=2.0.0
|
||||||
|
|
||||||
|
# Extra libraries are installed when needed using pipmaster
|
||||||
|
|
||||||
pipmaster
|
pipmaster
|
||||||
pydantic
|
pydantic
|
||||||
python-dotenv
|
python-dotenv
|
||||||
@@ -13,5 +19,4 @@ tenacity
|
|||||||
|
|
||||||
# LLM packages
|
# LLM packages
|
||||||
tiktoken
|
tiktoken
|
||||||
|
xlsxwriter>=3.1.0
|
||||||
# Extra libraries are installed when needed using pipmaster
|
|
||||||
|
Reference in New Issue
Block a user