Merge branch 'improve-property-tooltip' into feat-node-expand
This commit is contained in:
commit
db183a6395
43 changed files with 1586 additions and 398 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-DRGuXfZw.css
Normal file
1
lightrag/api/webui/assets/index-DRGuXfZw.css
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" type="image/svg+xml" href="./logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lightrag</title>
|
||||
<script type="module" crossorigin src="./assets/index-C_HczF2h.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CQ75jPFs.css">
|
||||
<script type="module" crossorigin src="./assets/index-B9TRs-Wk.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DRGuXfZw.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -127,6 +127,30 @@ class BaseVectorStorage(StorageNameSpace, ABC):
|
|||
async def delete_entity_relation(self, entity_name: str) -> None:
|
||||
"""Delete relations for a given entity."""
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseKVStorage(StorageNameSpace, ABC):
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ class ChromaVectorDBStorage(BaseVectorStorage):
|
|||
logger.error(f"Error during ChromaDB upsert: {str(e)}")
|
||||
raise
|
||||
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
try:
|
||||
embedding = await self.embedding_func([query])
|
||||
|
||||
|
|
@ -269,3 +271,67 @@ class ChromaVectorDBStorage(BaseVectorStorage):
|
|||
except Exception as e:
|
||||
logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
|
||||
raise
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
try:
|
||||
# Query the collection for a single vector by ID
|
||||
result = self._collection.get(
|
||||
ids=[id], include=["metadatas", "embeddings", "documents"]
|
||||
)
|
||||
|
||||
if not result or not result["ids"] or len(result["ids"]) == 0:
|
||||
return None
|
||||
|
||||
# Format the result to match the expected structure
|
||||
return {
|
||||
"id": result["ids"][0],
|
||||
"vector": result["embeddings"][0],
|
||||
"content": result["documents"][0],
|
||||
**result["metadatas"][0],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Query the collection for multiple vectors by IDs
|
||||
result = self._collection.get(
|
||||
ids=ids, include=["metadatas", "embeddings", "documents"]
|
||||
)
|
||||
|
||||
if not result or not result["ids"] or len(result["ids"]) == 0:
|
||||
return []
|
||||
|
||||
# Format the results to match the expected structure
|
||||
return [
|
||||
{
|
||||
"id": result["ids"][i],
|
||||
"vector": result["embeddings"][i],
|
||||
"content": result["documents"][i],
|
||||
**result["metadatas"][i],
|
||||
}
|
||||
for i in range(len(result["ids"]))
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -171,7 +171,9 @@ class FaissVectorDBStorage(BaseVectorStorage):
|
|||
logger.info(f"Upserted {len(list_data)} vectors into Faiss index.")
|
||||
return [m["__id__"] for m in list_data]
|
||||
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Search by a textual query; returns top_k results with their metadata + similarity distance.
|
||||
"""
|
||||
|
|
@ -392,3 +394,46 @@ class FaissVectorDBStorage(BaseVectorStorage):
|
|||
|
||||
logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
|
||||
return matching_records
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
# Find the Faiss internal ID for the custom ID
|
||||
fid = self._find_faiss_id_by_custom_id(id)
|
||||
if fid is None:
|
||||
return None
|
||||
|
||||
# Get the metadata for the found ID
|
||||
metadata = self._id_to_meta.get(fid, {})
|
||||
if not metadata:
|
||||
return None
|
||||
|
||||
return {**metadata, "id": metadata.get("__id__")}
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
results = []
|
||||
for id in ids:
|
||||
fid = self._find_faiss_id_by_custom_id(id)
|
||||
if fid is not None:
|
||||
metadata = self._id_to_meta.get(fid, {})
|
||||
if metadata:
|
||||
results.append({**metadata, "id": metadata.get("__id__")})
|
||||
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -101,7 +101,9 @@ class MilvusVectorDBStorage(BaseVectorStorage):
|
|||
results = self._client.upsert(collection_name=self.namespace, data=list_data)
|
||||
return results
|
||||
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
embedding = await self.embedding_func([query])
|
||||
results = self._client.search(
|
||||
collection_name=self.namespace,
|
||||
|
|
@ -231,3 +233,57 @@ class MilvusVectorDBStorage(BaseVectorStorage):
|
|||
except Exception as e:
|
||||
logger.error(f"Error searching for records with prefix '{prefix}': {e}")
|
||||
return []
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
try:
|
||||
# Query Milvus for a specific ID
|
||||
result = self._client.query(
|
||||
collection_name=self.namespace,
|
||||
filter=f'id == "{id}"',
|
||||
output_fields=list(self.meta_fields) + ["id"],
|
||||
)
|
||||
|
||||
if not result or len(result) == 0:
|
||||
return None
|
||||
|
||||
return result[0]
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Prepare the ID filter expression
|
||||
id_list = '", "'.join(ids)
|
||||
filter_expr = f'id in ["{id_list}"]'
|
||||
|
||||
# Query Milvus with the filter
|
||||
result = self._client.query(
|
||||
collection_name=self.namespace,
|
||||
filter=filter_expr,
|
||||
output_fields=list(self.meta_fields) + ["id"],
|
||||
)
|
||||
|
||||
return result or []
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -938,7 +938,9 @@ class MongoVectorDBStorage(BaseVectorStorage):
|
|||
|
||||
return list_data
|
||||
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Queries the vector database using Atlas Vector Search."""
|
||||
# Generate the embedding
|
||||
embedding = await self.embedding_func([query])
|
||||
|
|
@ -1071,6 +1073,59 @@ class MongoVectorDBStorage(BaseVectorStorage):
|
|||
logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
|
||||
return []
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
try:
|
||||
# Search for the specific ID in MongoDB
|
||||
result = await self._data.find_one({"_id": id})
|
||||
if result:
|
||||
# Format the result to include id field expected by API
|
||||
result_dict = dict(result)
|
||||
if "_id" in result_dict and "id" not in result_dict:
|
||||
result_dict["id"] = result_dict["_id"]
|
||||
return result_dict
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Query MongoDB for multiple IDs
|
||||
cursor = self._data.find({"_id": {"$in": ids}})
|
||||
results = await cursor.to_list(length=None)
|
||||
|
||||
# Format results to include id field expected by API
|
||||
formatted_results = []
|
||||
for result in results:
|
||||
result_dict = dict(result)
|
||||
if "_id" in result_dict and "id" not in result_dict:
|
||||
result_dict["id"] = result_dict["_id"]
|
||||
formatted_results.append(result_dict)
|
||||
|
||||
return formatted_results
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
|
||||
collection_names = await db.list_collection_names()
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ class NanoVectorDBStorage(BaseVectorStorage):
|
|||
f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}"
|
||||
)
|
||||
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
# Execute embedding outside of lock to avoid long lock times
|
||||
embedding = await self.embedding_func([query])
|
||||
embedding = embedding[0]
|
||||
|
|
@ -256,3 +258,33 @@ class NanoVectorDBStorage(BaseVectorStorage):
|
|||
|
||||
logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
|
||||
return matching_records
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
client = await self._get_client()
|
||||
result = client.get([id])
|
||||
if result:
|
||||
return result[0]
|
||||
return None
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
client = await self._get_client()
|
||||
return client.get(ids)
|
||||
|
|
|
|||
|
|
@ -417,7 +417,9 @@ class OracleVectorDBStorage(BaseVectorStorage):
|
|||
self.db = None
|
||||
|
||||
#################### query method ###############
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
embeddings = await self.embedding_func([query])
|
||||
embedding = embeddings[0]
|
||||
# 转换精度
|
||||
|
|
@ -529,6 +531,80 @@ class OracleVectorDBStorage(BaseVectorStorage):
|
|||
logger.error(f"Error searching records with prefix '{prefix}': {e}")
|
||||
return []
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
try:
|
||||
# Determine the table name based on namespace
|
||||
table_name = namespace_to_table_name(self.namespace)
|
||||
if not table_name:
|
||||
logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
|
||||
return None
|
||||
|
||||
# Create the appropriate ID field name based on namespace
|
||||
id_field = "entity_id" if "NODES" in table_name else "relation_id"
|
||||
if "CHUNKS" in table_name:
|
||||
id_field = "chunk_id"
|
||||
|
||||
# Prepare and execute the query
|
||||
query = f"""
|
||||
SELECT * FROM {table_name}
|
||||
WHERE {id_field} = :id AND workspace = :workspace
|
||||
"""
|
||||
params = {"id": id, "workspace": self.db.workspace}
|
||||
|
||||
result = await self.db.query(query, params)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Determine the table name based on namespace
|
||||
table_name = namespace_to_table_name(self.namespace)
|
||||
if not table_name:
|
||||
logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
|
||||
return []
|
||||
|
||||
# Create the appropriate ID field name based on namespace
|
||||
id_field = "entity_id" if "NODES" in table_name else "relation_id"
|
||||
if "CHUNKS" in table_name:
|
||||
id_field = "chunk_id"
|
||||
|
||||
# Format the list of IDs for SQL IN clause
|
||||
ids_list = ", ".join([f"'{id}'" for id in ids])
|
||||
|
||||
# Prepare and execute the query
|
||||
query = f"""
|
||||
SELECT * FROM {table_name}
|
||||
WHERE {id_field} IN ({ids_list}) AND workspace = :workspace
|
||||
"""
|
||||
params = {"workspace": self.db.workspace}
|
||||
|
||||
results = await self.db.query(query, params, multirows=True)
|
||||
return results or []
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@final
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -621,6 +621,60 @@ class PGVectorStorage(BaseVectorStorage):
|
|||
logger.error(f"Error during prefix search for '{prefix}': {e}")
|
||||
return []
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
table_name = namespace_to_table_name(self.namespace)
|
||||
if not table_name:
|
||||
logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
|
||||
return None
|
||||
|
||||
query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id=$2"
|
||||
params = {"workspace": self.db.workspace, "id": id}
|
||||
|
||||
try:
|
||||
result = await self.db.query(query, params)
|
||||
if result:
|
||||
return dict(result)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
table_name = namespace_to_table_name(self.namespace)
|
||||
if not table_name:
|
||||
logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
|
||||
return []
|
||||
|
||||
ids_str = ",".join([f"'{id}'" for id in ids])
|
||||
query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id IN ({ids_str})"
|
||||
params = {"workspace": self.db.workspace}
|
||||
|
||||
try:
|
||||
results = await self.db.query(query, params, multirows=True)
|
||||
return [dict(record) for record in results]
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@final
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -123,7 +123,9 @@ class QdrantVectorDBStorage(BaseVectorStorage):
|
|||
)
|
||||
return results
|
||||
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
embedding = await self.embedding_func([query])
|
||||
results = self._client.search(
|
||||
collection_name=self.namespace,
|
||||
|
|
|
|||
|
|
@ -306,7 +306,9 @@ class TiDBVectorDBStorage(BaseVectorStorage):
|
|||
await ClientManager.release_client(self.db)
|
||||
self.db = None
|
||||
|
||||
async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
|
||||
async def query(
|
||||
self, query: str, top_k: int, ids: list[str] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Search from tidb vector"""
|
||||
embeddings = await self.embedding_func([query])
|
||||
embedding = embeddings[0]
|
||||
|
|
@ -463,6 +465,100 @@ class TiDBVectorDBStorage(BaseVectorStorage):
|
|||
logger.error(f"Error searching records with prefix '{prefix}': {e}")
|
||||
return []
|
||||
|
||||
async def get_by_id(self, id: str) -> dict[str, Any] | None:
|
||||
"""Get vector data by its ID
|
||||
|
||||
Args:
|
||||
id: The unique identifier of the vector
|
||||
|
||||
Returns:
|
||||
The vector data if found, or None if not found
|
||||
"""
|
||||
try:
|
||||
# Determine which table to query based on namespace
|
||||
if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
|
||||
sql_template = """
|
||||
SELECT entity_id as id, name as entity_name, entity_type, description, content
|
||||
FROM LIGHTRAG_GRAPH_NODES
|
||||
WHERE entity_id = :entity_id AND workspace = :workspace
|
||||
"""
|
||||
params = {"entity_id": id, "workspace": self.db.workspace}
|
||||
elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
|
||||
sql_template = """
|
||||
SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
|
||||
keywords, description, content
|
||||
FROM LIGHTRAG_GRAPH_EDGES
|
||||
WHERE relation_id = :relation_id AND workspace = :workspace
|
||||
"""
|
||||
params = {"relation_id": id, "workspace": self.db.workspace}
|
||||
elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
|
||||
sql_template = """
|
||||
SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
|
||||
FROM LIGHTRAG_DOC_CHUNKS
|
||||
WHERE chunk_id = :chunk_id AND workspace = :workspace
|
||||
"""
|
||||
params = {"chunk_id": id, "workspace": self.db.workspace}
|
||||
else:
|
||||
logger.warning(
|
||||
f"Namespace {self.namespace} not supported for get_by_id"
|
||||
)
|
||||
return None
|
||||
|
||||
result = await self.db.query(sql_template, params=params)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for ID {id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get multiple vector data by their IDs
|
||||
|
||||
Args:
|
||||
ids: List of unique identifiers
|
||||
|
||||
Returns:
|
||||
List of vector data objects that were found
|
||||
"""
|
||||
if not ids:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Format IDs for SQL IN clause
|
||||
ids_str = ", ".join([f"'{id}'" for id in ids])
|
||||
|
||||
# Determine which table to query based on namespace
|
||||
if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
|
||||
sql_template = f"""
|
||||
SELECT entity_id as id, name as entity_name, entity_type, description, content
|
||||
FROM LIGHTRAG_GRAPH_NODES
|
||||
WHERE entity_id IN ({ids_str}) AND workspace = :workspace
|
||||
"""
|
||||
elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
|
||||
sql_template = f"""
|
||||
SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
|
||||
keywords, description, content
|
||||
FROM LIGHTRAG_GRAPH_EDGES
|
||||
WHERE relation_id IN ({ids_str}) AND workspace = :workspace
|
||||
"""
|
||||
elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
|
||||
sql_template = f"""
|
||||
SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
|
||||
FROM LIGHTRAG_DOC_CHUNKS
|
||||
WHERE chunk_id IN ({ids_str}) AND workspace = :workspace
|
||||
"""
|
||||
else:
|
||||
logger.warning(
|
||||
f"Namespace {self.namespace} not supported for get_by_ids"
|
||||
)
|
||||
return []
|
||||
|
||||
params = {"workspace": self.db.workspace}
|
||||
results = await self.db.query(sql_template, params=params, multirows=True)
|
||||
return results if results else []
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
|
||||
return []
|
||||
|
||||
|
||||
@final
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -1710,19 +1710,7 @@ class LightRAG:
|
|||
async def get_entity_info(
|
||||
self, entity_name: str, include_vector_data: bool = False
|
||||
) -> dict[str, str | None | dict[str, str]]:
|
||||
"""Get detailed information of an entity
|
||||
|
||||
Args:
|
||||
entity_name: Entity name (no need for quotes)
|
||||
include_vector_data: Whether to include data from the vector database
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing entity information, including:
|
||||
- entity_name: Entity name
|
||||
- source_id: Source document ID
|
||||
- graph_data: Complete node data from the graph database
|
||||
- vector_data: (optional) Data from the vector database
|
||||
"""
|
||||
"""Get detailed information of an entity"""
|
||||
|
||||
# Get information from the graph
|
||||
node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
|
||||
|
|
@ -1737,29 +1725,15 @@ class LightRAG:
|
|||
# Optional: Get vector database information
|
||||
if include_vector_data:
|
||||
entity_id = compute_mdhash_id(entity_name, prefix="ent-")
|
||||
vector_data = self.entities_vdb._client.get([entity_id])
|
||||
result["vector_data"] = vector_data[0] if vector_data else None
|
||||
vector_data = await self.entities_vdb.get_by_id(entity_id)
|
||||
result["vector_data"] = vector_data
|
||||
|
||||
return result
|
||||
|
||||
async def get_relation_info(
|
||||
self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
|
||||
) -> dict[str, str | None | dict[str, str]]:
|
||||
"""Get detailed information of a relationship
|
||||
|
||||
Args:
|
||||
src_entity: Source entity name (no need for quotes)
|
||||
tgt_entity: Target entity name (no need for quotes)
|
||||
include_vector_data: Whether to include data from the vector database
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing relationship information, including:
|
||||
- src_entity: Source entity name
|
||||
- tgt_entity: Target entity name
|
||||
- source_id: Source document ID
|
||||
- graph_data: Complete edge data from the graph database
|
||||
- vector_data: (optional) Data from the vector database
|
||||
"""
|
||||
"""Get detailed information of a relationship"""
|
||||
|
||||
# Get information from the graph
|
||||
edge_data = await self.chunk_entity_relation_graph.get_edge(
|
||||
|
|
@ -1777,8 +1751,8 @@ class LightRAG:
|
|||
# Optional: Get vector database information
|
||||
if include_vector_data:
|
||||
rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
|
||||
vector_data = self.relationships_vdb._client.get([rel_id])
|
||||
result["vector_data"] = vector_data[0] if vector_data else None
|
||||
vector_data = await self.relationships_vdb.get_by_id(rel_id)
|
||||
result["vector_data"] = vector_data
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -34,11 +34,13 @@
|
|||
"cmdk": "^1.0.4",
|
||||
"graphology": "^0.26.0",
|
||||
"graphology-generators": "^0.11.2",
|
||||
"i18next": "^24.2.2",
|
||||
"lucide-react": "^0.475.0",
|
||||
"minisearch": "^7.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.6",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^9.1.0",
|
||||
"react-number-format": "^5.4.3",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
|
|
@ -60,6 +62,7 @@
|
|||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-i18next": "^8.1.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
|
|
@ -441,6 +444,8 @@
|
|||
|
||||
"@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-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
|
||||
|
|
@ -765,8 +770,12 @@
|
|||
|
||||
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
|
||||
|
||||
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
|
@ -1093,6 +1102,8 @@
|
|||
|
||||
"react-dropzone": ["react-dropzone@14.3.6", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-U792j+x0rcwH/U/Slv/OBNU/LGFYbDLHKKiJoPhNaOianayZevCt4Y5S0CraPssH/6/wT6xhKDfzdXUgCBS0HQ=="],
|
||||
|
||||
"react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="],
|
||||
|
|
@ -1271,6 +1282,8 @@
|
|||
|
||||
"vite": ["vite@6.1.1", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA=="],
|
||||
|
||||
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@
|
|||
"cmdk": "^1.0.4",
|
||||
"graphology": "^0.26.0",
|
||||
"graphology-generators": "^0.11.2",
|
||||
"i18next": "^24.2.2",
|
||||
"lucide-react": "^0.475.0",
|
||||
"minisearch": "^7.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.6",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^9.1.0",
|
||||
"react-number-format": "^5.4.3",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
|
|
@ -69,6 +71,7 @@
|
|||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-i18next": "^8.1.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
|
|
|
|||
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'
|
||||
|
||||
type ThemeProviderProps = {
|
||||
|
|
@ -21,30 +21,32 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
|||
* Component that provides the theme state and setter function to its children.
|
||||
*/
|
||||
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(() => {
|
||||
const root = window.document.documentElement
|
||||
root.classList.remove('light', 'dark')
|
||||
|
||||
if (theme === 'system') {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
root.classList.add(systemTheme)
|
||||
setTheme(systemTheme)
|
||||
return
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(e.matches ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
root.classList.add(mediaQuery.matches ? 'dark' : 'light')
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
} else {
|
||||
root.classList.add(theme)
|
||||
}
|
||||
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
useSettingsStore.getState().setTheme(theme)
|
||||
setTheme(theme)
|
||||
}
|
||||
setTheme
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import useTheme from '@/hooks/useTheme'
|
|||
import { MoonIcon, SunIcon } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
import { controlButtonVariant } from '@/lib/constants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Component that toggles the theme between light and dark.
|
||||
|
|
@ -11,13 +12,14 @@ export default function ThemeToggle() {
|
|||
const { theme, setTheme } = useTheme()
|
||||
const setLight = useCallback(() => setTheme('light'), [setTheme])
|
||||
const setDark = useCallback(() => setTheme('dark'), [setTheme])
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (theme === 'dark') {
|
||||
return (
|
||||
<Button
|
||||
onClick={setLight}
|
||||
variant={controlButtonVariant}
|
||||
tooltip="Switch to light theme"
|
||||
tooltip={t('header.themeToggle.switchToLight')}
|
||||
size="icon"
|
||||
side="bottom"
|
||||
>
|
||||
|
|
@ -29,7 +31,7 @@ export default function ThemeToggle() {
|
|||
<Button
|
||||
onClick={setDark}
|
||||
variant={controlButtonVariant}
|
||||
tooltip="Switch to dark theme"
|
||||
tooltip={t('header.themeToggle.switchToDark')}
|
||||
size="icon"
|
||||
side="bottom"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -13,38 +13,40 @@ import { errorMessage } from '@/lib/utils'
|
|||
import { clearDocuments } from '@/api/lightrag'
|
||||
|
||||
import { EraserIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function ClearDocumentsDialog() {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleClear = useCallback(async () => {
|
||||
try {
|
||||
const result = await clearDocuments()
|
||||
if (result.status === 'success') {
|
||||
toast.success('Documents cleared successfully')
|
||||
toast.success(t('documentPanel.clearDocuments.success'))
|
||||
setOpen(false)
|
||||
} else {
|
||||
toast.error(`Clear Documents Failed:\n${result.message}`)
|
||||
toast.error(t('documentPanel.clearDocuments.failed', { message: result.message }))
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Clear Documents Failed:\n' + errorMessage(err))
|
||||
toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
|
||||
}
|
||||
}, [setOpen])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
|
||||
<EraserIcon/> Clear
|
||||
<Button variant="outline" side="bottom" tooltip={t('documentPanel.clearDocuments.tooltip')} size="sm">
|
||||
<EraserIcon/> {t('documentPanel.clearDocuments.button')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Clear documents</DialogTitle>
|
||||
<DialogDescription>Do you really want to clear all documents?</DialogDescription>
|
||||
<DialogTitle>{t('documentPanel.clearDocuments.title')}</DialogTitle>
|
||||
<DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Button variant="destructive" onClick={handleClear}>
|
||||
YES
|
||||
{t('documentPanel.clearDocuments.confirmButton')}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import { errorMessage } from '@/lib/utils'
|
|||
import { uploadDocument } from '@/api/lightrag'
|
||||
|
||||
import { UploadIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function UploadDocumentsDialog() {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [progresses, setProgresses] = useState<Record<string, number>>({})
|
||||
|
|
@ -29,24 +31,24 @@ export default function UploadDocumentsDialog() {
|
|||
filesToUpload.map(async (file) => {
|
||||
try {
|
||||
const result = await uploadDocument(file, (percentCompleted: number) => {
|
||||
console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
|
||||
console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted }))
|
||||
setProgresses((pre) => ({
|
||||
...pre,
|
||||
[file.name]: percentCompleted
|
||||
}))
|
||||
})
|
||||
if (result.status === 'success') {
|
||||
toast.success(`Upload Success:\n${file.name} uploaded successfully`)
|
||||
toast.success(t('documentPanel.uploadDocuments.success', { name: file.name }))
|
||||
} else {
|
||||
toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
|
||||
toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message }))
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
|
||||
toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) }))
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
toast.error('Upload Failed\n' + errorMessage(err))
|
||||
toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
// setOpen(false)
|
||||
|
|
@ -66,21 +68,21 @@ export default function UploadDocumentsDialog() {
|
|||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="default" side="bottom" tooltip="Upload documents" size="sm">
|
||||
<UploadIcon /> Upload
|
||||
<Button variant="default" side="bottom" tooltip={t('documentPanel.uploadDocuments.tooltip')} size="sm">
|
||||
<UploadIcon /> {t('documentPanel.uploadDocuments.button')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upload documents</DialogTitle>
|
||||
<DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Drag and drop your documents here or click to browse.
|
||||
{t('documentPanel.uploadDocuments.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FileUploader
|
||||
maxFileCount={Infinity}
|
||||
maxSize={200 * 1024 * 1024}
|
||||
description="supported types: 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"
|
||||
description={t('documentPanel.uploadDocuments.fileTypes')}
|
||||
onUpload={handleDocumentsUpload}
|
||||
progresses={progresses}
|
||||
disabled={isUploading}
|
||||
|
|
|
|||
|
|
@ -2,21 +2,23 @@ import { useFullScreen } from '@react-sigma/core'
|
|||
import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
|
||||
import { controlButtonVariant } from '@/lib/constants'
|
||||
import Button from '@/components/ui/Button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Component that toggles full screen mode.
|
||||
*/
|
||||
const FullScreenControl = () => {
|
||||
const { isFullScreen, toggle } = useFullScreen()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFullScreen ? (
|
||||
<Button variant={controlButtonVariant} onClick={toggle} tooltip="Windowed" size="icon">
|
||||
<Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.windowed')} size="icon">
|
||||
<MinimizeIcon />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant={controlButtonVariant} onClick={toggle} tooltip="Full Screen" size="icon">
|
||||
<Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.fullScreen')} size="icon">
|
||||
<MaximizeIcon />
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import { useSettingsStore } from '@/stores/settings'
|
|||
import { useGraphStore } from '@/stores/graph'
|
||||
import { labelListLimit } from '@/lib/constants'
|
||||
import MiniSearch from 'minisearch'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const GraphLabels = () => {
|
||||
const { t } = useTranslation()
|
||||
const label = useSettingsStore.use.queryLabel()
|
||||
const graphLabels = useGraphStore.use.graphLabels()
|
||||
|
||||
|
|
@ -45,7 +47,7 @@ const GraphLabels = () => {
|
|||
|
||||
return result.length <= labelListLimit
|
||||
? result
|
||||
: [...result.slice(0, labelListLimit), `And ${result.length - labelListLimit} others`]
|
||||
: [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
|
||||
},
|
||||
[getSearchEngine]
|
||||
)
|
||||
|
|
@ -68,14 +70,14 @@ const GraphLabels = () => {
|
|||
className="ml-2"
|
||||
triggerClassName="max-h-8"
|
||||
searchInputClassName="max-h-8"
|
||||
triggerTooltip="Select query label"
|
||||
triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
|
||||
fetcher={fetchData}
|
||||
renderOption={(item) => <div>{item}</div>}
|
||||
getOptionValue={(item) => item}
|
||||
getDisplayValue={(item) => <div>{item}</div>}
|
||||
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
||||
label="Label"
|
||||
placeholder="Search labels..."
|
||||
label={t('graphPanel.graphLabels.label')}
|
||||
placeholder={t('graphPanel.graphLabels.placeholder')}
|
||||
value={label !== null ? label : ''}
|
||||
onChange={setQueryLabel}
|
||||
clearable={false} // Prevent clearing value on reselect
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { AsyncSearch } from '@/components/ui/AsyncSearch'
|
|||
import { searchResultLimit } from '@/lib/constants'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import MiniSearch from 'minisearch'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface OptionItem {
|
||||
id: string
|
||||
|
|
@ -44,6 +45,7 @@ export const GraphSearchInput = ({
|
|||
onFocus?: GraphSearchInputProps['onFocus']
|
||||
value?: GraphSearchInputProps['value']
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const graph = useGraphStore.use.sigmaGraph()
|
||||
|
||||
const searchEngine = useMemo(() => {
|
||||
|
|
@ -97,7 +99,7 @@ export const GraphSearchInput = ({
|
|||
{
|
||||
type: 'message',
|
||||
id: messageId,
|
||||
message: `And ${result.length - searchResultLimit} others`
|
||||
message: t('graphPanel.search.message', { count: result.length - searchResultLimit })
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -118,7 +120,7 @@ export const GraphSearchInput = ({
|
|||
if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
|
||||
}}
|
||||
label={'item'}
|
||||
placeholder="Search nodes..."
|
||||
placeholder={t('graphPanel.search.placeholder')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { controlButtonVariant } from '@/lib/constants'
|
|||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type LayoutName =
|
||||
| 'Circular'
|
||||
|
|
@ -28,6 +29,7 @@ type LayoutName =
|
|||
const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
|
||||
const sigma = useSigma()
|
||||
const { stop, start, isRunning } = layout
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* Init component when Sigma or component settings change.
|
||||
|
|
@ -61,7 +63,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
|
|||
<Button
|
||||
size="icon"
|
||||
onClick={() => (isRunning ? stop() : start())}
|
||||
tooltip={isRunning ? 'Stop the layout animation' : 'Start the layout animation'}
|
||||
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
|
||||
variant={controlButtonVariant}
|
||||
>
|
||||
{isRunning ? <PauseIcon /> : <PlayIcon />}
|
||||
|
|
@ -74,6 +76,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
|
|||
*/
|
||||
const LayoutsControl = () => {
|
||||
const sigma = useSigma()
|
||||
const { t } = useTranslation()
|
||||
const [layout, setLayout] = useState<LayoutName>('Circular')
|
||||
const [opened, setOpened] = useState<boolean>(false)
|
||||
|
||||
|
|
@ -149,7 +152,7 @@ const LayoutsControl = () => {
|
|||
size="icon"
|
||||
variant={controlButtonVariant}
|
||||
onClick={() => setOpened((e: boolean) => !e)}
|
||||
tooltip="Layout Graph"
|
||||
tooltip={t('graphPanel.sideBar.layoutsControl.layoutGraph')}
|
||||
>
|
||||
<GripIcon />
|
||||
</Button>
|
||||
|
|
@ -166,7 +169,7 @@ const LayoutsControl = () => {
|
|||
key={name}
|
||||
className="cursor-pointer text-xs"
|
||||
>
|
||||
{name}
|
||||
{t(`graphPanel.sideBar.layoutsControl.layouts.${name}`)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
|||
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
|
||||
import Text from '@/components/ui/Text'
|
||||
import useLightragGraph from '@/hooks/useLightragGraph'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Component that view properties of elements in graph.
|
||||
|
|
@ -147,21 +148,22 @@ const PropertyRow = ({
|
|||
}
|
||||
|
||||
const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-sky-300">{t('graphPanel.propertiesView.node.title')}</label>
|
||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||
<PropertyRow name={'Id'} value={node.id} />
|
||||
<PropertyRow name={t('graphPanel.propertiesView.node.id')} value={node.id} />
|
||||
<PropertyRow
|
||||
name={'Labels'}
|
||||
name={t('graphPanel.propertiesView.node.labels')}
|
||||
value={node.labels.join(', ')}
|
||||
onClick={() => {
|
||||
useGraphStore.getState().setSelectedNode(node.id, true)
|
||||
}}
|
||||
/>
|
||||
<PropertyRow name={'Degree'} value={node.degree} />
|
||||
<PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
|
||||
</div>
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.node.properties')}</label>
|
||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||
{Object.keys(node.properties)
|
||||
.sort()
|
||||
|
|
@ -172,7 +174,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|||
{node.relationships.length > 0 && (
|
||||
<>
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
|
||||
Relationships
|
||||
{t('graphPanel.propertiesView.node.relationships')}
|
||||
</label>
|
||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||
{node.relationships.map(({ type, id, label }) => {
|
||||
|
|
@ -195,28 +197,29 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
|
|||
}
|
||||
|
||||
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">{t('graphPanel.propertiesView.edge.title')}</label>
|
||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||
<PropertyRow name={'Id'} value={edge.id} />
|
||||
{edge.type && <PropertyRow name={'Type'} value={edge.type} />}
|
||||
<PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
|
||||
{edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
|
||||
<PropertyRow
|
||||
name={'Source'}
|
||||
name={t('graphPanel.propertiesView.edge.source')}
|
||||
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
|
||||
onClick={() => {
|
||||
useGraphStore.getState().setSelectedNode(edge.source, true)
|
||||
}}
|
||||
/>
|
||||
<PropertyRow
|
||||
name={'Target'}
|
||||
name={t('graphPanel.propertiesView.edge.target')}
|
||||
value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
|
||||
onClick={() => {
|
||||
useGraphStore.getState().setSelectedNode(edge.target, true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
|
||||
<label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.edge.properties')}</label>
|
||||
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
|
||||
{Object.keys(edge.properties)
|
||||
.sort()
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'
|
|||
import { useBackendState } from '@/stores/state'
|
||||
|
||||
import { SettingsIcon, RefreshCwIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* Component that displays a checkbox with a label.
|
||||
|
|
@ -205,11 +206,13 @@ export default function Settings() {
|
|||
[setTempApiKey]
|
||||
)
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={controlButtonVariant}
|
||||
tooltip="Refresh Layout"
|
||||
tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
|
||||
size="icon"
|
||||
onClick={refreshLayout}
|
||||
>
|
||||
|
|
@ -217,7 +220,7 @@ export default function Settings() {
|
|||
</Button>
|
||||
<Popover open={opened} onOpenChange={setOpened}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant={controlButtonVariant} tooltip="Settings" size="icon">
|
||||
<Button variant={controlButtonVariant} tooltip={t('graphPanel.sideBar.settings.settings')} size="icon">
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -231,7 +234,7 @@ export default function Settings() {
|
|||
<LabeledCheckBox
|
||||
checked={enableHealthCheck}
|
||||
onCheckedChange={setEnableHealthCheck}
|
||||
label="Health Check"
|
||||
label={t('graphPanel.sideBar.settings.healthCheck')}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
|
@ -239,12 +242,12 @@ export default function Settings() {
|
|||
<LabeledCheckBox
|
||||
checked={showPropertyPanel}
|
||||
onCheckedChange={setShowPropertyPanel}
|
||||
label="Show Property Panel"
|
||||
label={t('graphPanel.sideBar.settings.showPropertyPanel')}
|
||||
/>
|
||||
<LabeledCheckBox
|
||||
checked={showNodeSearchBar}
|
||||
onCheckedChange={setShowNodeSearchBar}
|
||||
label="Show Search Bar"
|
||||
label={t('graphPanel.sideBar.settings.showSearchBar')}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
|
@ -252,12 +255,12 @@ export default function Settings() {
|
|||
<LabeledCheckBox
|
||||
checked={showNodeLabel}
|
||||
onCheckedChange={setShowNodeLabel}
|
||||
label="Show Node Label"
|
||||
label={t('graphPanel.sideBar.settings.showNodeLabel')}
|
||||
/>
|
||||
<LabeledCheckBox
|
||||
checked={enableNodeDrag}
|
||||
onCheckedChange={setEnableNodeDrag}
|
||||
label="Node Draggable"
|
||||
label={t('graphPanel.sideBar.settings.nodeDraggable')}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
|
@ -265,51 +268,50 @@ export default function Settings() {
|
|||
<LabeledCheckBox
|
||||
checked={showEdgeLabel}
|
||||
onCheckedChange={setShowEdgeLabel}
|
||||
label="Show Edge Label"
|
||||
label={t('graphPanel.sideBar.settings.showEdgeLabel')}
|
||||
/>
|
||||
<LabeledCheckBox
|
||||
checked={enableHideUnselectedEdges}
|
||||
onCheckedChange={setEnableHideUnselectedEdges}
|
||||
label="Hide Unselected Edges"
|
||||
label={t('graphPanel.sideBar.settings.hideUnselectedEdges')}
|
||||
/>
|
||||
<LabeledCheckBox
|
||||
checked={enableEdgeEvents}
|
||||
onCheckedChange={setEnableEdgeEvents}
|
||||
label="Edge Events"
|
||||
label={t('graphPanel.sideBar.settings.edgeEvents')}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
<LabeledNumberInput
|
||||
label="Max Query Depth"
|
||||
label={t('graphPanel.sideBar.settings.maxQueryDepth')}
|
||||
min={1}
|
||||
value={graphQueryMaxDepth}
|
||||
onEditFinished={setGraphQueryMaxDepth}
|
||||
/>
|
||||
<LabeledNumberInput
|
||||
label="Minimum Degree"
|
||||
label={t('graphPanel.sideBar.settings.minDegree')}
|
||||
min={0}
|
||||
value={graphMinDegree}
|
||||
onEditFinished={setGraphMinDegree}
|
||||
/>
|
||||
<LabeledNumberInput
|
||||
label="Max Layout Iterations"
|
||||
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">API Key</label>
|
||||
<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="Enter your API key"
|
||||
placeholder={t('graphPanel.sideBar.settings.enterYourAPIkey')}
|
||||
className="max-h-full w-full min-w-0"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
|
@ -320,7 +322,7 @@ export default function Settings() {
|
|||
size="sm"
|
||||
className="max-h-full shrink-0"
|
||||
>
|
||||
Save
|
||||
{t('graphPanel.sideBar.settings.save')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
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>Depth: {graphQueryMaxDepth}</div>
|
||||
<div>Degree: {graphMinDegree}</div>
|
||||
<div>{t('graphPanel.sideBar.settings.depth')}: {graphQueryMaxDepth}</div>
|
||||
<div>{t('graphPanel.sideBar.settings.degree')}: {graphMinDegree}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,60 @@
|
|||
import { LightragStatus } from '@/api/lightrag'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const StatusCard = ({ status }: { status: LightragStatus | null }) => {
|
||||
const { t } = useTranslation()
|
||||
if (!status) {
|
||||
return <div className="text-muted-foreground text-sm">Status information unavailable</div>
|
||||
return <div className="text-muted-foreground text-sm">{t('graphPanel.statusCard.unavailable')}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-[300px] space-y-3 text-sm">
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">Storage Info</h4>
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.storageInfo')}</h4>
|
||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||
<span>Working Directory:</span>
|
||||
<span>{t('graphPanel.statusCard.workingDirectory')}:</span>
|
||||
<span className="truncate">{status.working_directory}</span>
|
||||
<span>Input Directory:</span>
|
||||
<span>{t('graphPanel.statusCard.inputDirectory')}:</span>
|
||||
<span className="truncate">{status.input_directory}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">LLM Configuration</h4>
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.llmConfig')}</h4>
|
||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||
<span>LLM Binding:</span>
|
||||
<span>{t('graphPanel.statusCard.llmBinding')}:</span>
|
||||
<span>{status.configuration.llm_binding}</span>
|
||||
<span>LLM Binding Host:</span>
|
||||
<span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
|
||||
<span>{status.configuration.llm_binding_host}</span>
|
||||
<span>LLM Model:</span>
|
||||
<span>{t('graphPanel.statusCard.llmModel')}:</span>
|
||||
<span>{status.configuration.llm_model}</span>
|
||||
<span>Max Tokens:</span>
|
||||
<span>{t('graphPanel.statusCard.maxTokens')}:</span>
|
||||
<span>{status.configuration.max_tokens}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">Embedding Configuration</h4>
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.embeddingConfig')}</h4>
|
||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||
<span>Embedding Binding:</span>
|
||||
<span>{t('graphPanel.statusCard.embeddingBinding')}:</span>
|
||||
<span>{status.configuration.embedding_binding}</span>
|
||||
<span>Embedding Binding Host:</span>
|
||||
<span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
|
||||
<span>{status.configuration.embedding_binding_host}</span>
|
||||
<span>Embedding Model:</span>
|
||||
<span>{t('graphPanel.statusCard.embeddingModel')}:</span>
|
||||
<span>{status.configuration.embedding_model}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium">Storage Configuration</h4>
|
||||
<h4 className="font-medium">{t('graphPanel.statusCard.storageConfig')}</h4>
|
||||
<div className="text-muted-foreground grid grid-cols-2 gap-1">
|
||||
<span>KV Storage:</span>
|
||||
<span>{t('graphPanel.statusCard.kvStorage')}:</span>
|
||||
<span>{status.configuration.kv_storage}</span>
|
||||
<span>Doc Status Storage:</span>
|
||||
<span>{t('graphPanel.statusCard.docStatusStorage')}:</span>
|
||||
<span>{status.configuration.doc_status_storage}</span>
|
||||
<span>Graph Storage:</span>
|
||||
<span>{t('graphPanel.statusCard.graphStorage')}:</span>
|
||||
<span>{status.configuration.graph_storage}</span>
|
||||
<span>Vector Storage:</span>
|
||||
<span>{t('graphPanel.statusCard.vectorStorage')}:</span>
|
||||
<span>{status.configuration.vector_storage}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { useBackendState } from '@/stores/state'
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||
import StatusCard from '@/components/graph/StatusCard'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const StatusIndicator = () => {
|
||||
const { t } = useTranslation()
|
||||
const health = useBackendState.use.health()
|
||||
const lastCheckTime = useBackendState.use.lastCheckTime()
|
||||
const status = useBackendState.use.status()
|
||||
|
|
@ -33,7 +35,7 @@ const StatusIndicator = () => {
|
|||
)}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{health ? 'Connected' : 'Disconnected'}
|
||||
{health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')}
|
||||
</span>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { useCallback } from 'react'
|
|||
import Button from '@/components/ui/Button'
|
||||
import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
|
||||
import { controlButtonVariant } from '@/lib/constants'
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Component that provides zoom controls for the graph viewer.
|
||||
*/
|
||||
const ZoomControl = () => {
|
||||
const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
|
||||
const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
|
||||
|
|
@ -16,16 +18,16 @@ const ZoomControl = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip="Zoom In" size="icon">
|
||||
<Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip={t("graphPanel.sideBar.zoomControl.zoomIn")} size="icon">
|
||||
<ZoomInIcon />
|
||||
</Button>
|
||||
<Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip="Zoom Out" size="icon">
|
||||
<Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip={t("graphPanel.sideBar.zoomControl.zoomOut")} size="icon">
|
||||
<ZoomOutIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant={controlButtonVariant}
|
||||
onClick={handleResetZoom}
|
||||
tooltip="Reset Zoom"
|
||||
tooltip={t("graphPanel.sideBar.zoomControl.resetZoom")}
|
||||
size="icon"
|
||||
>
|
||||
<FullscreenIcon />
|
||||
|
|
|
|||
|
|
@ -15,18 +15,21 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|||
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
||||
|
||||
import { LoaderIcon, CopyIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type MessageWithError = Message & {
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
export const ChatMessage = ({ message }: { message: MessageWithError }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleCopyMarkdown = useCallback(async () => {
|
||||
if (message.content) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(message.content)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
console.error(t('chat.copyError'), err)
|
||||
}
|
||||
}
|
||||
}, [message])
|
||||
|
|
@ -57,7 +60,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => {
|
|||
<Button
|
||||
onClick={handleCopyMarkdown}
|
||||
className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
|
||||
tooltip="Copy to clipboard"
|
||||
tooltip={t('retrievePanel.chatMessage.copyTooltip')}
|
||||
variant="default"
|
||||
size="icon"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import {
|
|||
SelectValue
|
||||
} from '@/components/ui/Select'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function QuerySettings() {
|
||||
const { t } = useTranslation()
|
||||
const querySettings = useSettingsStore((state) => state.querySettings)
|
||||
|
||||
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
|
||||
|
|
@ -25,8 +27,8 @@ export default function QuerySettings() {
|
|||
return (
|
||||
<Card className="flex shrink-0 flex-col">
|
||||
<CardHeader className="px-4 pt-4 pb-2">
|
||||
<CardTitle>Parameters</CardTitle>
|
||||
<CardDescription>Configure your query parameters</CardDescription>
|
||||
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
||||
<CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
||||
<div className="relative size-full">
|
||||
|
|
@ -35,8 +37,8 @@ export default function QuerySettings() {
|
|||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Query Mode"
|
||||
tooltip="Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval"
|
||||
text={t('retrievePanel.querySettings.queryMode')}
|
||||
tooltip={t('retrievePanel.querySettings.queryModeTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Select
|
||||
|
|
@ -48,11 +50,11 @@ export default function QuerySettings() {
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="naive">Naive</SelectItem>
|
||||
<SelectItem value="local">Local</SelectItem>
|
||||
<SelectItem value="global">Global</SelectItem>
|
||||
<SelectItem value="hybrid">Hybrid</SelectItem>
|
||||
<SelectItem value="mix">Mix</SelectItem>
|
||||
<SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
|
||||
<SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
|
||||
<SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
|
||||
<SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
|
||||
<SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -62,8 +64,8 @@ export default function QuerySettings() {
|
|||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Response Format"
|
||||
tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
|
||||
text={t('retrievePanel.querySettings.responseFormat')}
|
||||
tooltip={t('retrievePanel.querySettings.responseFormatTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Select
|
||||
|
|
@ -75,9 +77,9 @@ export default function QuerySettings() {
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem>
|
||||
<SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
|
||||
<SelectItem value="Bullet Points">Bullet Points</SelectItem>
|
||||
<SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
|
||||
<SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
|
||||
<SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
|
@ -87,8 +89,8 @@ export default function QuerySettings() {
|
|||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Top K Results"
|
||||
tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
|
||||
text={t('retrievePanel.querySettings.topK')}
|
||||
tooltip={t('retrievePanel.querySettings.topKTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
|
|
@ -97,7 +99,7 @@ export default function QuerySettings() {
|
|||
value={querySettings.top_k}
|
||||
onValueChange={(v) => handleChange('top_k', v)}
|
||||
min={1}
|
||||
placeholder="Number of results"
|
||||
placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
|
||||
/>
|
||||
</>
|
||||
|
||||
|
|
@ -106,8 +108,8 @@ export default function QuerySettings() {
|
|||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Max Tokens for Text Unit"
|
||||
tooltip="Maximum number of tokens allowed for each retrieved text chunk"
|
||||
text={t('retrievePanel.querySettings.maxTokensTextUnit')}
|
||||
tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
|
|
@ -116,14 +118,14 @@ export default function QuerySettings() {
|
|||
value={querySettings.max_token_for_text_unit}
|
||||
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
|
||||
min={1}
|
||||
placeholder="Max tokens for text unit"
|
||||
placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
|
||||
/>
|
||||
</>
|
||||
|
||||
<>
|
||||
<Text
|
||||
text="Max Tokens for Global Context"
|
||||
tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
|
||||
text={t('retrievePanel.querySettings.maxTokensGlobalContext')}
|
||||
tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
|
|
@ -132,15 +134,15 @@ export default function QuerySettings() {
|
|||
value={querySettings.max_token_for_global_context}
|
||||
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
|
||||
min={1}
|
||||
placeholder="Max tokens for global context"
|
||||
placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
|
||||
/>
|
||||
</>
|
||||
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Max Tokens for Local Context"
|
||||
tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
|
||||
text={t('retrievePanel.querySettings.maxTokensLocalContext')}
|
||||
tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
|
|
@ -149,7 +151,7 @@ export default function QuerySettings() {
|
|||
value={querySettings.max_token_for_local_context}
|
||||
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
|
||||
min={1}
|
||||
placeholder="Max tokens for local context"
|
||||
placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
|
|
@ -158,8 +160,8 @@ export default function QuerySettings() {
|
|||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="History Turns"
|
||||
tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
|
||||
text={t('retrievePanel.querySettings.historyTurns')}
|
||||
tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<NumberInput
|
||||
|
|
@ -170,7 +172,7 @@ export default function QuerySettings() {
|
|||
value={querySettings.history_turns}
|
||||
onValueChange={(v) => handleChange('history_turns', v)}
|
||||
min={0}
|
||||
placeholder="Number of history turns"
|
||||
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
|
||||
/>
|
||||
</>
|
||||
|
||||
|
|
@ -179,8 +181,8 @@ export default function QuerySettings() {
|
|||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="High-Level Keywords"
|
||||
tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
|
||||
text={t('retrievePanel.querySettings.hlKeywords')}
|
||||
tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Input
|
||||
|
|
@ -194,15 +196,15 @@ export default function QuerySettings() {
|
|||
.filter((k) => k !== '')
|
||||
handleChange('hl_keywords', keywords)
|
||||
}}
|
||||
placeholder="Enter keywords"
|
||||
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
|
||||
/>
|
||||
</>
|
||||
|
||||
<>
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Low-Level Keywords"
|
||||
tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
|
||||
text={t('retrievePanel.querySettings.llKeywords')}
|
||||
tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<Input
|
||||
|
|
@ -216,7 +218,7 @@ export default function QuerySettings() {
|
|||
.filter((k) => k !== '')
|
||||
handleChange('ll_keywords', keywords)
|
||||
}}
|
||||
placeholder="Enter keywords"
|
||||
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
|
|
@ -226,8 +228,8 @@ export default function QuerySettings() {
|
|||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Only Need Context"
|
||||
tooltip="If True, only returns the retrieved context without generating a response"
|
||||
text={t('retrievePanel.querySettings.onlyNeedContext')}
|
||||
tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<div className="grow" />
|
||||
|
|
@ -242,8 +244,8 @@ export default function QuerySettings() {
|
|||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Only Need Prompt"
|
||||
tooltip="If True, only returns the generated prompt without producing a response"
|
||||
text={t('retrievePanel.querySettings.onlyNeedPrompt')}
|
||||
tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<div className="grow" />
|
||||
|
|
@ -258,8 +260,8 @@ export default function QuerySettings() {
|
|||
<div className="flex items-center gap-2">
|
||||
<Text
|
||||
className="ml-1"
|
||||
text="Stream Response"
|
||||
tooltip="If True, enables streaming output for real-time responses"
|
||||
text={t('retrievePanel.querySettings.streamResponse')}
|
||||
tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
|
||||
side="left"
|
||||
/>
|
||||
<div className="grow" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/components/ui/Button'
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -22,6 +23,7 @@ import { useBackendState } from '@/stores/state'
|
|||
import { RefreshCwIcon } from 'lucide-react'
|
||||
|
||||
export default function DocumentManager() {
|
||||
const { t } = useTranslation()
|
||||
const health = useBackendState.use.health()
|
||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||
|
||||
|
|
@ -44,7 +46,7 @@ export default function DocumentManager() {
|
|||
setDocs(null)
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Failed to load documents\n' + errorMessage(err))
|
||||
toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
|
||||
}
|
||||
}, [setDocs])
|
||||
|
||||
|
|
@ -57,7 +59,7 @@ export default function DocumentManager() {
|
|||
const { status } = await scanNewDocuments()
|
||||
toast.message(status)
|
||||
} catch (err) {
|
||||
toast.error('Failed to load documents\n' + errorMessage(err))
|
||||
toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
@ -69,7 +71,7 @@ export default function DocumentManager() {
|
|||
try {
|
||||
await fetchDocuments()
|
||||
} catch (err) {
|
||||
toast.error('Failed to get scan progress\n' + errorMessage(err))
|
||||
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
||||
}
|
||||
}, 5000)
|
||||
return () => clearInterval(interval)
|
||||
|
|
@ -78,7 +80,7 @@ export default function DocumentManager() {
|
|||
return (
|
||||
<Card className="!size-full !rounded-none !border-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Document Management</CardTitle>
|
||||
<CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -86,10 +88,10 @@ export default function DocumentManager() {
|
|||
variant="outline"
|
||||
onClick={scanDocuments}
|
||||
side="bottom"
|
||||
tooltip="Scan documents"
|
||||
tooltip={t('documentPanel.documentManager.scanTooltip')}
|
||||
size="sm"
|
||||
>
|
||||
<RefreshCwIcon /> Scan
|
||||
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<ClearDocumentsDialog />
|
||||
|
|
@ -98,29 +100,29 @@ export default function DocumentManager() {
|
|||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Uploaded documents</CardTitle>
|
||||
<CardDescription>view the uploaded documents here</CardDescription>
|
||||
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
|
||||
<CardDescription>{t('documentPanel.documentManager.uploadedDescription')}</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{!docs && (
|
||||
<EmptyCard
|
||||
title="No documents uploaded"
|
||||
description="upload documents to see them here"
|
||||
title={t('documentPanel.documentManager.emptyTitle')}
|
||||
description={t('documentPanel.documentManager.emptyDescription')}
|
||||
/>
|
||||
)}
|
||||
{docs && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Summary</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Length</TableHead>
|
||||
<TableHead>Chunks</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Updated</TableHead>
|
||||
<TableHead>Metadata</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.id')}</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.created')}</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.updated')}</TableHead>
|
||||
<TableHead>{t('documentPanel.documentManager.columns.metadata')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="text-sm">
|
||||
|
|
@ -137,13 +139,13 @@ export default function DocumentManager() {
|
|||
</TableCell>
|
||||
<TableCell>
|
||||
{status === 'processed' && (
|
||||
<span className="text-green-600">Completed</span>
|
||||
<span className="text-green-600">{t('documentPanel.documentManager.status.completed')}</span>
|
||||
)}
|
||||
{status === 'processing' && (
|
||||
<span className="text-blue-600">Processing</span>
|
||||
<span className="text-blue-600">{t('documentPanel.documentManager.status.processing')}</span>
|
||||
)}
|
||||
{status === 'pending' && <span className="text-yellow-600">Pending</span>}
|
||||
{status === 'failed' && <span className="text-red-600">Failed</span>}
|
||||
{status === 'pending' && <span className="text-yellow-600">{t('documentPanel.documentManager.status.pending')}</span>}
|
||||
{status === 'failed' && <span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>}
|
||||
{doc.error && (
|
||||
<span className="ml-2 text-red-500" title={doc.error}>
|
||||
⚠️
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import { useDebounce } from '@/hooks/useDebounce'
|
|||
import QuerySettings from '@/components/retrieval/QuerySettings'
|
||||
import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
|
||||
import { EraserIcon, SendIcon } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RetrievalTesting() {
|
||||
const { t } = useTranslation()
|
||||
const [messages, setMessages] = useState<MessageWithError[]>(
|
||||
() => useSettingsStore.getState().retrievalHistory || []
|
||||
)
|
||||
|
|
@ -89,7 +91,7 @@ export default function RetrievalTesting() {
|
|||
}
|
||||
} catch (err) {
|
||||
// Handle error
|
||||
updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`, true)
|
||||
updateAssistantMessage(`${t('retrievePanel.retrieval.error')}\n${errorMessage(err)}`, true)
|
||||
} finally {
|
||||
// Clear loading and add messages to state
|
||||
setIsLoading(false)
|
||||
|
|
@ -98,7 +100,7 @@ export default function RetrievalTesting() {
|
|||
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
|
||||
}
|
||||
},
|
||||
[inputValue, isLoading, messages, setMessages]
|
||||
[inputValue, isLoading, messages, setMessages, t]
|
||||
)
|
||||
|
||||
const debouncedMessages = useDebounce(messages, 100)
|
||||
|
|
@ -117,7 +119,7 @@ export default function RetrievalTesting() {
|
|||
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-muted-foreground flex h-full items-center justify-center text-lg">
|
||||
Start a retrieval by typing your query below
|
||||
{t('retrievePanel.retrieval.startPrompt')}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message, idx) => (
|
||||
|
|
@ -143,18 +145,18 @@ export default function RetrievalTesting() {
|
|||
size="sm"
|
||||
>
|
||||
<EraserIcon />
|
||||
Clear
|
||||
{t('retrievePanel.retrieval.clear')}
|
||||
</Button>
|
||||
<Input
|
||||
className="flex-1"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
placeholder="Type your query..."
|
||||
placeholder={t('retrievePanel.retrieval.placeholder')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Button type="submit" variant="default" disabled={isLoading} size="sm">
|
||||
<SendIcon />
|
||||
Send
|
||||
{t('retrievePanel.retrieval.send')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import Button from '@/components/ui/Button'
|
||||
import { SiteInfo } from '@/lib/constants'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import AppSettings from '@/components/AppSettings'
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ZapIcon, GithubIcon } from 'lucide-react'
|
||||
|
||||
|
|
@ -29,21 +30,22 @@ function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
|
|||
|
||||
function TabsNavigation() {
|
||||
const currentTab = useSettingsStore.use.currentTab()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex h-8 self-center">
|
||||
<TabsList className="h-full gap-2">
|
||||
<NavigationTab value="documents" currentTab={currentTab}>
|
||||
Documents
|
||||
{t('header.documents')}
|
||||
</NavigationTab>
|
||||
<NavigationTab value="knowledge-graph" currentTab={currentTab}>
|
||||
Knowledge Graph
|
||||
{t('header.knowledgeGraph')}
|
||||
</NavigationTab>
|
||||
<NavigationTab value="retrieval" currentTab={currentTab}>
|
||||
Retrieval
|
||||
{t('header.retrieval')}
|
||||
</NavigationTab>
|
||||
<NavigationTab value="api" currentTab={currentTab}>
|
||||
API
|
||||
{t('header.api')}
|
||||
</NavigationTab>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
|
@ -51,6 +53,7 @@ function TabsNavigation() {
|
|||
}
|
||||
|
||||
export default function SiteHeader() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
|
||||
<a href="/" className="mr-6 flex items-center gap-2">
|
||||
|
|
@ -64,12 +67,14 @@ export default function SiteHeader() {
|
|||
</div>
|
||||
|
||||
<nav className="flex items-center">
|
||||
<Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
|
||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<ThemeToggle />
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
|
||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<AppSettings />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
|
|
|
|||
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
|
||||
244
lightrag_webui/src/locales/en.json
Normal file
244
lightrag_webui/src/locales/en.json
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
{
|
||||
"settings": {
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"header": {
|
||||
"documents": "Documents",
|
||||
"knowledgeGraph": "Knowledge Graph",
|
||||
"retrieval": "Retrieval",
|
||||
"api": "API",
|
||||
"projectRepository": "Project Repository",
|
||||
"themeToggle": {
|
||||
"switchToLight": "Switch to light theme",
|
||||
"switchToDark": "Switch to dark theme"
|
||||
}
|
||||
},
|
||||
"documentPanel": {
|
||||
"clearDocuments": {
|
||||
"button": "Clear",
|
||||
"tooltip": "Clear documents",
|
||||
"title": "Clear Documents",
|
||||
"confirm": "Do you really want to clear all documents?",
|
||||
"confirmButton": "YES",
|
||||
"success": "Documents cleared successfully",
|
||||
"failed": "Clear Documents Failed:\n{{message}}",
|
||||
"error": "Clear Documents Failed:\n{{error}}"
|
||||
},
|
||||
"uploadDocuments": {
|
||||
"button": "Upload",
|
||||
"tooltip": "Upload documents",
|
||||
"title": "Upload Documents",
|
||||
"description": "Drag and drop your documents here or click to browse.",
|
||||
"uploading": "Uploading {{name}}: {{percent}}%",
|
||||
"success": "Upload Success:\n{{name}} uploaded successfully",
|
||||
"failed": "Upload Failed:\n{{name}}\n{{message}}",
|
||||
"error": "Upload Failed:\n{{name}}\n{{error}}",
|
||||
"generalError": "Upload Failed\n{{error}}",
|
||||
"fileTypes": "Supported types: 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": {
|
||||
"title": "Document Management",
|
||||
"scanButton": "Scan",
|
||||
"scanTooltip": "Scan documents",
|
||||
"uploadedTitle": "Uploaded Documents",
|
||||
"uploadedDescription": "List of uploaded documents and their statuses.",
|
||||
"emptyTitle": "No Documents",
|
||||
"emptyDescription": "There are no uploaded documents yet.",
|
||||
"columns": {
|
||||
"id": "ID",
|
||||
"summary": "Summary",
|
||||
"status": "Status",
|
||||
"length": "Length",
|
||||
"chunks": "Chunks",
|
||||
"created": "Created",
|
||||
"updated": "Updated",
|
||||
"metadata": "Metadata"
|
||||
},
|
||||
"status": {
|
||||
"completed": "Completed",
|
||||
"processing": "Processing",
|
||||
"pending": "Pending",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "Failed to load documents\n{{error}}",
|
||||
"scanFailed": "Failed to scan documents\n{{error}}",
|
||||
"scanProgressFailed": "Failed to get scan progress\n{{error}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphPanel": {
|
||||
"sideBar": {
|
||||
"settings": {
|
||||
"settings": "Settings",
|
||||
"healthCheck": "Health Check",
|
||||
"showPropertyPanel": "Show Property Panel",
|
||||
"showSearchBar": "Show Search Bar",
|
||||
"showNodeLabel": "Show Node Label",
|
||||
"nodeDraggable": "Node Draggable",
|
||||
"showEdgeLabel": "Show Edge Label",
|
||||
"hideUnselectedEdges": "Hide Unselected Edges",
|
||||
"edgeEvents": "Edge Events",
|
||||
"maxQueryDepth": "Max Query Depth",
|
||||
"minDegree": "Minimum Degree",
|
||||
"maxLayoutIterations": "Max Layout Iterations",
|
||||
"depth": "Depth",
|
||||
"degree": "Degree",
|
||||
"apiKey": "API Key",
|
||||
"enterYourAPIkey": "Enter your API key",
|
||||
"save": "Save",
|
||||
"refreshLayout": "Refresh Layout"
|
||||
},
|
||||
|
||||
"zoomControl": {
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetZoom": "Reset Zoom"
|
||||
},
|
||||
|
||||
"layoutsControl": {
|
||||
"startAnimation": "Start the layout animation",
|
||||
"stopAnimation": "Stop the layout animation",
|
||||
"layoutGraph": "Layout Graph",
|
||||
"layouts": {
|
||||
"Circular": "Circular",
|
||||
"Circlepack": "Circlepack",
|
||||
"Random": "Random",
|
||||
"Noverlaps": "Noverlaps",
|
||||
"Force Directed": "Force Directed",
|
||||
"Force Atlas": "Force Atlas"
|
||||
}
|
||||
},
|
||||
|
||||
"fullScreenControl": {
|
||||
"fullScreen": "Full Screen",
|
||||
"windowed": "Windowed"
|
||||
}
|
||||
},
|
||||
"statusIndicator": {
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected"
|
||||
},
|
||||
"statusCard": {
|
||||
"unavailable": "Status information unavailable",
|
||||
"storageInfo": "Storage Info",
|
||||
"workingDirectory": "Working Directory",
|
||||
"inputDirectory": "Input Directory",
|
||||
"llmConfig": "LLM Configuration",
|
||||
"llmBinding": "LLM Binding",
|
||||
"llmBindingHost": "LLM Binding Host",
|
||||
"llmModel": "LLM Model",
|
||||
"maxTokens": "Max Tokens",
|
||||
"embeddingConfig": "Embedding Configuration",
|
||||
"embeddingBinding": "Embedding Binding",
|
||||
"embeddingBindingHost": "Embedding Binding Host",
|
||||
"embeddingModel": "Embedding Model",
|
||||
"storageConfig": "Storage Configuration",
|
||||
"kvStorage": "KV Storage",
|
||||
"docStatusStorage": "Doc Status Storage",
|
||||
"graphStorage": "Graph Storage",
|
||||
"vectorStorage": "Vector Storage"
|
||||
},
|
||||
"propertiesView": {
|
||||
"node": {
|
||||
"title": "Node",
|
||||
"id": "ID",
|
||||
"labels": "Labels",
|
||||
"degree": "Degree",
|
||||
"properties": "Properties",
|
||||
"relationships": "Relationships"
|
||||
},
|
||||
"edge": {
|
||||
"title": "Relationship",
|
||||
"id": "ID",
|
||||
"type": "Type",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"properties": "Properties"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search nodes...",
|
||||
"message": "And {count} others"
|
||||
},
|
||||
"graphLabels": {
|
||||
"selectTooltip": "Select query label",
|
||||
"noLabels": "No labels found",
|
||||
"label": "Label",
|
||||
"placeholder": "Search labels...",
|
||||
"andOthers": "And {count} others"
|
||||
}
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
"copyTooltip": "Copy to clipboard",
|
||||
"copyError": "Failed to copy text to clipboard"
|
||||
},
|
||||
"retrieval": {
|
||||
"startPrompt": "Start a retrieval by typing your query below",
|
||||
"clear": "Clear",
|
||||
"send": "Send",
|
||||
"placeholder": "Type your query...",
|
||||
"error": "Error: Failed to get response"
|
||||
},
|
||||
"querySettings": {
|
||||
"parametersTitle": "Parameters",
|
||||
"parametersDescription": "Configure your query parameters",
|
||||
|
||||
"queryMode": "Query Mode",
|
||||
"queryModeTooltip": "Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval",
|
||||
"queryModeOptions": {
|
||||
"naive": "Naive",
|
||||
"local": "Local",
|
||||
"global": "Global",
|
||||
"hybrid": "Hybrid",
|
||||
"mix": "Mix"
|
||||
},
|
||||
|
||||
"responseFormat": "Response Format",
|
||||
"responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points",
|
||||
"responseFormatOptions": {
|
||||
"multipleParagraphs": "Multiple Paragraphs",
|
||||
"singleParagraph": "Single Paragraph",
|
||||
"bulletPoints": "Bullet Points"
|
||||
},
|
||||
|
||||
"topK": "Top K Results",
|
||||
"topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode",
|
||||
"topKPlaceholder": "Number of results",
|
||||
|
||||
"maxTokensTextUnit": "Max Tokens for Text Unit",
|
||||
"maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk",
|
||||
|
||||
"maxTokensGlobalContext": "Max Tokens for Global Context",
|
||||
"maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval",
|
||||
|
||||
"maxTokensLocalContext": "Max Tokens for Local Context",
|
||||
"maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval",
|
||||
|
||||
"historyTurns": "History Turns",
|
||||
"historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context",
|
||||
"historyTurnsPlaceholder": "Number of history turns",
|
||||
|
||||
"hlKeywords": "High-Level Keywords",
|
||||
"hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas",
|
||||
"hlkeywordsPlaceHolder": "Enter keywords",
|
||||
|
||||
"llKeywords": "Low-Level Keywords",
|
||||
"llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas",
|
||||
|
||||
"onlyNeedContext": "Only Need Context",
|
||||
"onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response",
|
||||
|
||||
"onlyNeedPrompt": "Only Need Prompt",
|
||||
"onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response",
|
||||
|
||||
"streamResponse": "Stream Response",
|
||||
"streamResponseTooltip": "If True, enables streaming output for real-time responses"
|
||||
}
|
||||
}
|
||||
}
|
||||
229
lightrag_webui/src/locales/zh.json
Normal file
229
lightrag_webui/src/locales/zh.json
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
{
|
||||
"settings": {
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "系统"
|
||||
},
|
||||
"header": {
|
||||
"documents": "文档",
|
||||
"knowledgeGraph": "知识图谱",
|
||||
"retrieval": "检索",
|
||||
"api": "API",
|
||||
"projectRepository": "项目仓库",
|
||||
"themeToggle": {
|
||||
"switchToLight": "切换到浅色主题",
|
||||
"switchToDark": "切换到深色主题"
|
||||
}
|
||||
},
|
||||
"documentPanel": {
|
||||
"clearDocuments": {
|
||||
"button": "清空",
|
||||
"tooltip": "清空文档",
|
||||
"title": "清空文档",
|
||||
"confirm": "确定要清空所有文档吗?",
|
||||
"confirmButton": "确定",
|
||||
"success": "文档清空成功",
|
||||
"failed": "清空文档失败:\n{{message}}",
|
||||
"error": "清空文档失败:\n{{error}}"
|
||||
},
|
||||
"uploadDocuments": {
|
||||
"button": "上传",
|
||||
"tooltip": "上传文档",
|
||||
"title": "上传文档",
|
||||
"description": "拖拽文件到此处或点击浏览",
|
||||
"uploading": "正在上传 {{name}}:{{percent}}%",
|
||||
"success": "上传成功:\n{{name}} 上传完成",
|
||||
"failed": "上传失败:\n{{name}}\n{{message}}",
|
||||
"error": "上传失败:\n{{name}}\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"
|
||||
},
|
||||
"documentManager": {
|
||||
"title": "文档管理",
|
||||
"scanButton": "扫描",
|
||||
"scanTooltip": "扫描文档",
|
||||
"uploadedTitle": "已上传文档",
|
||||
"uploadedDescription": "已上传文档列表及其状态",
|
||||
"emptyTitle": "无文档",
|
||||
"emptyDescription": "还没有上传任何文档",
|
||||
"columns": {
|
||||
"id": "ID",
|
||||
"summary": "摘要",
|
||||
"status": "状态",
|
||||
"length": "长度",
|
||||
"chunks": "分块",
|
||||
"created": "创建时间",
|
||||
"updated": "更新时间",
|
||||
"metadata": "元数据"
|
||||
},
|
||||
"status": {
|
||||
"completed": "已完成",
|
||||
"processing": "处理中",
|
||||
"pending": "等待中",
|
||||
"failed": "失败"
|
||||
},
|
||||
"errors": {
|
||||
"loadFailed": "加载文档失败\n{{error}}",
|
||||
"scanFailed": "扫描文档失败\n{{error}}",
|
||||
"scanProgressFailed": "获取扫描进度失败\n{{error}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"graphPanel": {
|
||||
"sideBar": {
|
||||
"settings": {
|
||||
"settings": "设置",
|
||||
"healthCheck": "健康检查",
|
||||
"showPropertyPanel": "显示属性面板",
|
||||
"showSearchBar": "显示搜索栏",
|
||||
"showNodeLabel": "显示节点标签",
|
||||
"nodeDraggable": "节点可拖动",
|
||||
"showEdgeLabel": "显示边标签",
|
||||
"hideUnselectedEdges": "隐藏未选中的边",
|
||||
"edgeEvents": "边事件",
|
||||
"maxQueryDepth": "最大查询深度",
|
||||
"minDegree": "最小度数",
|
||||
"maxLayoutIterations": "最大布局迭代次数",
|
||||
"depth": "深度",
|
||||
"degree": "邻边",
|
||||
"apiKey": "API密钥",
|
||||
"enterYourAPIkey": "输入您的API密钥",
|
||||
"save": "保存",
|
||||
"refreshLayout": "刷新布局"
|
||||
},
|
||||
"zoomControl": {
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小",
|
||||
"resetZoom": "重置缩放"
|
||||
},
|
||||
"layoutsControl": {
|
||||
"startAnimation": "开始布局动画",
|
||||
"stopAnimation": "停止布局动画",
|
||||
"layoutGraph": "图布局",
|
||||
"layouts": {
|
||||
"Circular": "环形",
|
||||
"Circlepack": "圆形打包",
|
||||
"Random": "随机",
|
||||
"Noverlaps": "无重叠",
|
||||
"Force Directed": "力导向",
|
||||
"Force Atlas": "力图"
|
||||
}
|
||||
},
|
||||
"fullScreenControl": {
|
||||
"fullScreen": "全屏",
|
||||
"windowed": "窗口"
|
||||
}
|
||||
},
|
||||
"statusIndicator": {
|
||||
"connected": "已连接",
|
||||
"disconnected": "未连接"
|
||||
},
|
||||
"statusCard": {
|
||||
"unavailable": "状态信息不可用",
|
||||
"storageInfo": "存储信息",
|
||||
"workingDirectory": "工作目录",
|
||||
"inputDirectory": "输入目录",
|
||||
"llmConfig": "LLM配置",
|
||||
"llmBinding": "LLM绑定",
|
||||
"llmBindingHost": "LLM绑定主机",
|
||||
"llmModel": "LLM模型",
|
||||
"maxTokens": "最大令牌数",
|
||||
"embeddingConfig": "嵌入配置",
|
||||
"embeddingBinding": "嵌入绑定",
|
||||
"embeddingBindingHost": "嵌入绑定主机",
|
||||
"embeddingModel": "嵌入模型",
|
||||
"storageConfig": "存储配置",
|
||||
"kvStorage": "KV存储",
|
||||
"docStatusStorage": "文档状态存储",
|
||||
"graphStorage": "图存储",
|
||||
"vectorStorage": "向量存储"
|
||||
},
|
||||
"propertiesView": {
|
||||
"node": {
|
||||
"title": "节点",
|
||||
"id": "ID",
|
||||
"labels": "标签",
|
||||
"degree": "度数",
|
||||
"properties": "属性",
|
||||
"relationships": "关系"
|
||||
},
|
||||
"edge": {
|
||||
"title": "关系",
|
||||
"id": "ID",
|
||||
"type": "类型",
|
||||
"source": "源节点",
|
||||
"target": "目标节点",
|
||||
"properties": "属性"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索节点...",
|
||||
"message": "还有 {count} 个"
|
||||
},
|
||||
"graphLabels": {
|
||||
"selectTooltip": "选择查询标签",
|
||||
"noLabels": "未找到标签",
|
||||
"label": "标签",
|
||||
"placeholder": "搜索标签...",
|
||||
"andOthers": "还有 {count} 个"
|
||||
}
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
"copyTooltip": "复制到剪贴板",
|
||||
"copyError": "复制文本到剪贴板失败"
|
||||
},
|
||||
"retrieval": {
|
||||
"startPrompt": "输入查询开始检索",
|
||||
"clear": "清空",
|
||||
"send": "发送",
|
||||
"placeholder": "输入查询...",
|
||||
"error": "错误:获取响应失败"
|
||||
},
|
||||
"querySettings": {
|
||||
"parametersTitle": "参数",
|
||||
"parametersDescription": "配置查询参数",
|
||||
"queryMode": "查询模式",
|
||||
"queryModeTooltip": "选择检索策略:\n• Naive:基础搜索,无高级技术\n• Local:上下文相关信息检索\n• Global:利用全局知识库\n• Hybrid:结合本地和全局检索\n• Mix:整合知识图谱和向量检索",
|
||||
"queryModeOptions": {
|
||||
"naive": "朴素",
|
||||
"local": "本地",
|
||||
"global": "全局",
|
||||
"hybrid": "混合",
|
||||
"mix": "混合"
|
||||
},
|
||||
"responseFormat": "响应格式",
|
||||
"responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点",
|
||||
"responseFormatOptions": {
|
||||
"multipleParagraphs": "多段落",
|
||||
"singleParagraph": "单段落",
|
||||
"bulletPoints": "要点"
|
||||
},
|
||||
"topK": "Top K结果",
|
||||
"topKTooltip": "检索的顶部项目数。在'local'模式下表示实体,在'global'模式下表示关系",
|
||||
"topKPlaceholder": "结果数量",
|
||||
"maxTokensTextUnit": "文本单元最大令牌数",
|
||||
"maxTokensTextUnitTooltip": "每个检索文本块允许的最大令牌数",
|
||||
"maxTokensGlobalContext": "全局上下文最大令牌数",
|
||||
"maxTokensGlobalContextTooltip": "全局检索中关系描述的最大令牌数",
|
||||
"maxTokensLocalContext": "本地上下文最大令牌数",
|
||||
"maxTokensLocalContextTooltip": "本地检索中实体描述的最大令牌数",
|
||||
"historyTurns": "历史轮次",
|
||||
"historyTurnsTooltip": "响应上下文中考虑的完整对话轮次(用户-助手对)数量",
|
||||
"historyTurnsPlaceholder": "历史轮次数",
|
||||
"hlKeywords": "高级关键词",
|
||||
"hlKeywordsTooltip": "检索中优先考虑的高级关键词列表。用逗号分隔",
|
||||
"hlkeywordsPlaceHolder": "输入关键词",
|
||||
"llKeywords": "低级关键词",
|
||||
"llKeywordsTooltip": "用于细化检索重点的低级关键词列表。用逗号分隔",
|
||||
"onlyNeedContext": "仅需上下文",
|
||||
"onlyNeedContextTooltip": "如果为True,仅返回检索到的上下文而不生成响应",
|
||||
"onlyNeedPrompt": "仅需提示",
|
||||
"onlyNeedPromptTooltip": "如果为True,仅返回生成的提示而不产生响应",
|
||||
"streamResponse": "流式响应",
|
||||
"streamResponseTooltip": "如果为True,启用实时流式输出响应"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,5 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { Root } from '@/components/Root'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
)
|
||||
createRoot(document.getElementById('root')!).render(<Root />)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Message, QueryRequest } from '@/api/lightrag'
|
|||
import { useGraphStore } from '@/stores/graph'
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system'
|
||||
type Language = 'en' | 'zh'
|
||||
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
||||
|
||||
interface SettingsState {
|
||||
|
|
@ -48,6 +49,9 @@ interface SettingsState {
|
|||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
|
||||
language: Language
|
||||
setLanguage: (lang: Language) => void
|
||||
|
||||
enableHealthCheck: boolean
|
||||
setEnableHealthCheck: (enable: boolean) => void
|
||||
|
||||
|
|
@ -55,10 +59,27 @@ interface SettingsState {
|
|||
setCurrentTab: (tab: Tab) => void
|
||||
}
|
||||
|
||||
// Helper to get initial state from localStorage
|
||||
const getInitialState = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem('settings-storage')
|
||||
if (stored) {
|
||||
const { state } = JSON.parse(stored)
|
||||
return {
|
||||
theme: state?.theme || 'system',
|
||||
language: state?.language || 'zh'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse settings from localStorage:', e)
|
||||
}
|
||||
return { theme: 'system', language: 'zh' }
|
||||
}
|
||||
|
||||
const useSettingsStoreBase = create<SettingsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: 'system',
|
||||
...getInitialState(),
|
||||
refreshLayout: () => {
|
||||
const graphState = useGraphStore.getState();
|
||||
const currentGraph = graphState.sigmaGraph;
|
||||
|
|
@ -110,6 +131,16 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||
|
||||
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) =>
|
||||
set({
|
||||
graphLayoutMaxIterations: iterations
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue