Merge branch 'main' into fix-neo4j-duplicate-nodes

This commit is contained in:
yangdx
2025-03-12 12:35:36 +08:00
34 changed files with 1219 additions and 197 deletions

View File

@@ -127,6 +127,30 @@ class BaseVectorStorage(StorageNameSpace, ABC):
async def delete_entity_relation(self, entity_name: str) -> None: async def delete_entity_relation(self, entity_name: str) -> None:
"""Delete relations for a given entity.""" """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 @dataclass
class BaseKVStorage(StorageNameSpace, ABC): class BaseKVStorage(StorageNameSpace, ABC):

View File

@@ -156,7 +156,9 @@ class ChromaVectorDBStorage(BaseVectorStorage):
logger.error(f"Error during ChromaDB upsert: {str(e)}") logger.error(f"Error during ChromaDB upsert: {str(e)}")
raise 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: try:
embedding = await self.embedding_func([query]) embedding = await self.embedding_func([query])
@@ -269,3 +271,67 @@ class ChromaVectorDBStorage(BaseVectorStorage):
except Exception as e: except Exception as e:
logger.error(f"Error during prefix search in ChromaDB: {str(e)}") logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
raise 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 []

View File

@@ -171,7 +171,9 @@ class FaissVectorDBStorage(BaseVectorStorage):
logger.info(f"Upserted {len(list_data)} vectors into Faiss index.") logger.info(f"Upserted {len(list_data)} vectors into Faiss index.")
return [m["__id__"] for m in list_data] 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. 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}'") logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
return matching_records 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

View File

@@ -101,7 +101,9 @@ class MilvusVectorDBStorage(BaseVectorStorage):
results = self._client.upsert(collection_name=self.namespace, data=list_data) results = self._client.upsert(collection_name=self.namespace, data=list_data)
return results 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]) embedding = await self.embedding_func([query])
results = self._client.search( results = self._client.search(
collection_name=self.namespace, collection_name=self.namespace,
@@ -231,3 +233,57 @@ class MilvusVectorDBStorage(BaseVectorStorage):
except Exception as e: except Exception as e:
logger.error(f"Error searching for records with prefix '{prefix}': {e}") logger.error(f"Error searching for records with prefix '{prefix}': {e}")
return [] 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 []

View File

@@ -938,7 +938,9 @@ class MongoVectorDBStorage(BaseVectorStorage):
return list_data 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.""" """Queries the vector database using Atlas Vector Search."""
# Generate the embedding # Generate the embedding
embedding = await self.embedding_func([query]) 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)}") logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
return [] 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): async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
collection_names = await db.list_collection_names() collection_names = await db.list_collection_names()

View File

@@ -120,7 +120,9 @@ class NanoVectorDBStorage(BaseVectorStorage):
f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}" 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 # Execute embedding outside of lock to avoid long lock times
embedding = await self.embedding_func([query]) embedding = await self.embedding_func([query])
embedding = embedding[0] embedding = embedding[0]
@@ -256,3 +258,33 @@ class NanoVectorDBStorage(BaseVectorStorage):
logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'") logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
return matching_records 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)

View File

@@ -417,7 +417,9 @@ class OracleVectorDBStorage(BaseVectorStorage):
self.db = None self.db = None
#################### query method ############### #################### 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]) embeddings = await self.embedding_func([query])
embedding = embeddings[0] embedding = embeddings[0]
# 转换精度 # 转换精度
@@ -529,6 +531,80 @@ class OracleVectorDBStorage(BaseVectorStorage):
logger.error(f"Error searching records with prefix '{prefix}': {e}") logger.error(f"Error searching records with prefix '{prefix}': {e}")
return [] 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 @final
@dataclass @dataclass

View File

@@ -621,6 +621,60 @@ class PGVectorStorage(BaseVectorStorage):
logger.error(f"Error during prefix search for '{prefix}': {e}") logger.error(f"Error during prefix search for '{prefix}': {e}")
return [] 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 @final
@dataclass @dataclass

View File

@@ -123,7 +123,9 @@ class QdrantVectorDBStorage(BaseVectorStorage):
) )
return results 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]) embedding = await self.embedding_func([query])
results = self._client.search( results = self._client.search(
collection_name=self.namespace, collection_name=self.namespace,

View File

@@ -306,7 +306,9 @@ class TiDBVectorDBStorage(BaseVectorStorage):
await ClientManager.release_client(self.db) await ClientManager.release_client(self.db)
self.db = None 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""" """Search from tidb vector"""
embeddings = await self.embedding_func([query]) embeddings = await self.embedding_func([query])
embedding = embeddings[0] embedding = embeddings[0]
@@ -463,6 +465,100 @@ class TiDBVectorDBStorage(BaseVectorStorage):
logger.error(f"Error searching records with prefix '{prefix}': {e}") logger.error(f"Error searching records with prefix '{prefix}': {e}")
return [] 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 @final
@dataclass @dataclass

View File

@@ -1710,19 +1710,7 @@ class LightRAG:
async def get_entity_info( async def get_entity_info(
self, entity_name: str, include_vector_data: bool = False self, entity_name: str, include_vector_data: bool = False
) -> dict[str, str | None | dict[str, str]]: ) -> dict[str, str | None | dict[str, str]]:
"""Get detailed information of an entity """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 information from the graph # Get information from the graph
node_data = await self.chunk_entity_relation_graph.get_node(entity_name) node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
@@ -1737,29 +1725,15 @@ class LightRAG:
# Optional: Get vector database information # Optional: Get vector database information
if include_vector_data: if include_vector_data:
entity_id = compute_mdhash_id(entity_name, prefix="ent-") entity_id = compute_mdhash_id(entity_name, prefix="ent-")
vector_data = self.entities_vdb._client.get([entity_id]) vector_data = await self.entities_vdb.get_by_id(entity_id)
result["vector_data"] = vector_data[0] if vector_data else None result["vector_data"] = vector_data
return result return result
async def get_relation_info( async def get_relation_info(
self, src_entity: str, tgt_entity: str, include_vector_data: bool = False self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
) -> dict[str, str | None | dict[str, str]]: ) -> dict[str, str | None | dict[str, str]]:
"""Get detailed information of a relationship """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 information from the graph # Get information from the graph
edge_data = await self.chunk_entity_relation_graph.get_edge( edge_data = await self.chunk_entity_relation_graph.get_edge(
@@ -1777,8 +1751,8 @@ class LightRAG:
# Optional: Get vector database information # Optional: Get vector database information
if include_vector_data: if include_vector_data:
rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-") rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
vector_data = self.relationships_vdb._client.get([rel_id]) vector_data = await self.relationships_vdb.get_by_id(rel_id)
result["vector_data"] = vector_data[0] if vector_data else None result["vector_data"] = vector_data
return result return result

View File

@@ -34,11 +34,13 @@
"cmdk": "^1.0.4", "cmdk": "^1.0.4",
"graphology": "^0.26.0", "graphology": "^0.26.0",
"graphology-generators": "^0.11.2", "graphology-generators": "^0.11.2",
"i18next": "^24.2.2",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"minisearch": "^7.1.2", "minisearch": "^7.1.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.6", "react-dropzone": "^14.3.6",
"react-i18next": "^15.4.1",
"react-markdown": "^9.1.0", "react-markdown": "^9.1.0",
"react-number-format": "^5.4.3", "react-number-format": "^5.4.3",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
@@ -765,8 +767,12 @@
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], "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=="], "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=="], "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=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -1093,6 +1099,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-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-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=="], "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 +1279,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=="], "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": ["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=="], "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=="],

View File

@@ -43,11 +43,13 @@
"cmdk": "^1.0.4", "cmdk": "^1.0.4",
"graphology": "^0.26.0", "graphology": "^0.26.0",
"graphology-generators": "^0.11.2", "graphology-generators": "^0.11.2",
"i18next": "^24.2.2",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"minisearch": "^7.1.2", "minisearch": "^7.1.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.6", "react-dropzone": "^14.3.6",
"react-i18next": "^15.4.1",
"react-markdown": "^9.1.0", "react-markdown": "^9.1.0",
"react-number-format": "^5.4.3", "react-number-format": "^5.4.3",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",

View File

@@ -3,6 +3,7 @@ import useTheme from '@/hooks/useTheme'
import { MoonIcon, SunIcon } from 'lucide-react' import { MoonIcon, SunIcon } from 'lucide-react'
import { useCallback } from 'react' import { useCallback } from 'react'
import { controlButtonVariant } from '@/lib/constants' import { controlButtonVariant } from '@/lib/constants'
import { useTranslation } from 'react-i18next'
/** /**
* Component that toggles the theme between light and dark. * Component that toggles the theme between light and dark.
@@ -11,13 +12,14 @@ export default function ThemeToggle() {
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const setLight = useCallback(() => setTheme('light'), [setTheme]) const setLight = useCallback(() => setTheme('light'), [setTheme])
const setDark = useCallback(() => setTheme('dark'), [setTheme]) const setDark = useCallback(() => setTheme('dark'), [setTheme])
const { t } = useTranslation()
if (theme === 'dark') { if (theme === 'dark') {
return ( return (
<Button <Button
onClick={setLight} onClick={setLight}
variant={controlButtonVariant} variant={controlButtonVariant}
tooltip="Switch to light theme" tooltip={t('header.themeToggle.switchToLight')}
size="icon" size="icon"
side="bottom" side="bottom"
> >
@@ -29,7 +31,7 @@ export default function ThemeToggle() {
<Button <Button
onClick={setDark} onClick={setDark}
variant={controlButtonVariant} variant={controlButtonVariant}
tooltip="Switch to dark theme" tooltip={t('header.themeToggle.switchToDark')}
size="icon" size="icon"
side="bottom" side="bottom"
> >

View File

@@ -13,38 +13,40 @@ import { errorMessage } from '@/lib/utils'
import { clearDocuments } from '@/api/lightrag' import { clearDocuments } from '@/api/lightrag'
import { EraserIcon } from 'lucide-react' import { EraserIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function ClearDocumentsDialog() { export default function ClearDocumentsDialog() {
const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const handleClear = useCallback(async () => { const handleClear = useCallback(async () => {
try { try {
const result = await clearDocuments() const result = await clearDocuments()
if (result.status === 'success') { if (result.status === 'success') {
toast.success('Documents cleared successfully') toast.success(t('documentPanel.clearDocuments.success'))
setOpen(false) setOpen(false)
} else { } else {
toast.error(`Clear Documents Failed:\n${result.message}`) toast.error(t('documentPanel.clearDocuments.failed', { message: result.message }))
} }
} catch (err) { } catch (err) {
toast.error('Clear Documents Failed:\n' + errorMessage(err)) toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
} }
}, [setOpen]) }, [setOpen])
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" side="bottom" tooltip='Clear documents' size="sm"> <Button variant="outline" side="bottom" tooltip={t('documentPanel.clearDocuments.tooltip')} size="sm">
<EraserIcon/> Clear <EraserIcon/> {t('documentPanel.clearDocuments.button')}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}> <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
<DialogHeader> <DialogHeader>
<DialogTitle>Clear documents</DialogTitle> <DialogTitle>{t('documentPanel.clearDocuments.title')}</DialogTitle>
<DialogDescription>Do you really want to clear all documents?</DialogDescription> <DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription>
</DialogHeader> </DialogHeader>
<Button variant="destructive" onClick={handleClear}> <Button variant="destructive" onClick={handleClear}>
YES {t('documentPanel.clearDocuments.confirmButton')}
</Button> </Button>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -14,8 +14,10 @@ import { errorMessage } from '@/lib/utils'
import { uploadDocument } from '@/api/lightrag' import { uploadDocument } from '@/api/lightrag'
import { UploadIcon } from 'lucide-react' import { UploadIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function UploadDocumentsDialog() { export default function UploadDocumentsDialog() {
const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
const [progresses, setProgresses] = useState<Record<string, number>>({}) const [progresses, setProgresses] = useState<Record<string, number>>({})
@@ -29,24 +31,24 @@ export default function UploadDocumentsDialog() {
filesToUpload.map(async (file) => { filesToUpload.map(async (file) => {
try { try {
const result = await uploadDocument(file, (percentCompleted: number) => { 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) => ({ setProgresses((pre) => ({
...pre, ...pre,
[file.name]: percentCompleted [file.name]: percentCompleted
})) }))
}) })
if (result.status === 'success') { if (result.status === 'success') {
toast.success(`Upload Success:\n${file.name} uploaded successfully`) toast.success(t('documentPanel.uploadDocuments.success', { name: file.name }))
} else { } 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) { } 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) { } catch (err) {
toast.error('Upload Failed\n' + errorMessage(err)) toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
} finally { } finally {
setIsUploading(false) setIsUploading(false)
// setOpen(false) // setOpen(false)
@@ -66,21 +68,21 @@ export default function UploadDocumentsDialog() {
}} }}
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="default" side="bottom" tooltip="Upload documents" size="sm"> <Button variant="default" side="bottom" tooltip={t('documentPanel.uploadDocuments.tooltip')} size="sm">
<UploadIcon /> Upload <UploadIcon /> {t('documentPanel.uploadDocuments.button')}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}> <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
<DialogHeader> <DialogHeader>
<DialogTitle>Upload documents</DialogTitle> <DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
<DialogDescription> <DialogDescription>
Drag and drop your documents here or click to browse. {t('documentPanel.uploadDocuments.description')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FileUploader <FileUploader
maxFileCount={Infinity} maxFileCount={Infinity}
maxSize={200 * 1024 * 1024} 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} onUpload={handleDocumentsUpload}
progresses={progresses} progresses={progresses}
disabled={isUploading} disabled={isUploading}

View File

@@ -2,21 +2,23 @@ import { useFullScreen } from '@react-sigma/core'
import { MaximizeIcon, MinimizeIcon } from 'lucide-react' import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
import { controlButtonVariant } from '@/lib/constants' import { controlButtonVariant } from '@/lib/constants'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { useTranslation } from 'react-i18next'
/** /**
* Component that toggles full screen mode. * Component that toggles full screen mode.
*/ */
const FullScreenControl = () => { const FullScreenControl = () => {
const { isFullScreen, toggle } = useFullScreen() const { isFullScreen, toggle } = useFullScreen()
const { t } = useTranslation()
return ( return (
<> <>
{isFullScreen ? ( {isFullScreen ? (
<Button variant={controlButtonVariant} onClick={toggle} tooltip="Windowed" size="icon"> <Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.windowed')} size="icon">
<MinimizeIcon /> <MinimizeIcon />
</Button> </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 /> <MaximizeIcon />
</Button> </Button>
)} )}

View File

@@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
import { labelListLimit } from '@/lib/constants' import { labelListLimit } from '@/lib/constants'
import MiniSearch from 'minisearch' import MiniSearch from 'minisearch'
import { useTranslation } from 'react-i18next'
const lastGraph: any = { const lastGraph: any = {
graph: null, graph: null,
@@ -13,6 +14,7 @@ const lastGraph: any = {
} }
const GraphLabels = () => { const GraphLabels = () => {
const { t } = useTranslation()
const label = useSettingsStore.use.queryLabel() const label = useSettingsStore.use.queryLabel()
const graph = useGraphStore.use.sigmaGraph() const graph = useGraphStore.use.sigmaGraph()
@@ -69,7 +71,7 @@ const GraphLabels = () => {
return result.length <= labelListLimit return result.length <= labelListLimit
? result ? result
: [...result.slice(0, labelListLimit), `And ${result.length - labelListLimit} others`] : [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
}, },
[getSearchEngine] [getSearchEngine]
) )
@@ -84,14 +86,14 @@ const GraphLabels = () => {
className="ml-2" className="ml-2"
triggerClassName="max-h-8" triggerClassName="max-h-8"
searchInputClassName="max-h-8" searchInputClassName="max-h-8"
triggerTooltip="Select query label" triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
fetcher={fetchData} fetcher={fetchData}
renderOption={(item) => <div>{item}</div>} renderOption={(item) => <div>{item}</div>}
getOptionValue={(item) => item} getOptionValue={(item) => item}
getDisplayValue={(item) => <div>{item}</div>} getDisplayValue={(item) => <div>{item}</div>}
notFound={<div className="py-6 text-center text-sm">No labels found</div>} notFound={<div className="py-6 text-center text-sm">No labels found</div>}
label="Label" label={t('graphPanel.graphLabels.label')}
placeholder="Search labels..." placeholder={t('graphPanel.graphLabels.placeholder')}
value={label !== null ? label : ''} value={label !== null ? label : ''}
onChange={setQueryLabel} onChange={setQueryLabel}
/> />

View File

@@ -9,6 +9,7 @@ import { AsyncSearch } from '@/components/ui/AsyncSearch'
import { searchResultLimit } from '@/lib/constants' import { searchResultLimit } from '@/lib/constants'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
import MiniSearch from 'minisearch' import MiniSearch from 'minisearch'
import { useTranslation } from 'react-i18next'
interface OptionItem { interface OptionItem {
id: string id: string
@@ -44,6 +45,7 @@ export const GraphSearchInput = ({
onFocus?: GraphSearchInputProps['onFocus'] onFocus?: GraphSearchInputProps['onFocus']
value?: GraphSearchInputProps['value'] value?: GraphSearchInputProps['value']
}) => { }) => {
const { t } = useTranslation()
const graph = useGraphStore.use.sigmaGraph() const graph = useGraphStore.use.sigmaGraph()
const searchEngine = useMemo(() => { const searchEngine = useMemo(() => {
@@ -97,7 +99,7 @@ export const GraphSearchInput = ({
{ {
type: 'message', type: 'message',
id: messageId, 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) if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
}} }}
label={'item'} label={'item'}
placeholder="Search nodes..." placeholder={t('graphPanel.search.placeholder')}
/> />
) )
} }

View File

@@ -16,6 +16,7 @@ import { controlButtonVariant } from '@/lib/constants'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react' import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
type LayoutName = type LayoutName =
| 'Circular' | 'Circular'
@@ -28,6 +29,7 @@ type LayoutName =
const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => { const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
const sigma = useSigma() const sigma = useSigma()
const { stop, start, isRunning } = layout const { stop, start, isRunning } = layout
const { t } = useTranslation()
/** /**
* Init component when Sigma or component settings change. * Init component when Sigma or component settings change.
@@ -61,7 +63,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
<Button <Button
size="icon" size="icon"
onClick={() => (isRunning ? stop() : start())} 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} variant={controlButtonVariant}
> >
{isRunning ? <PauseIcon /> : <PlayIcon />} {isRunning ? <PauseIcon /> : <PlayIcon />}
@@ -74,6 +76,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
*/ */
const LayoutsControl = () => { const LayoutsControl = () => {
const sigma = useSigma() const sigma = useSigma()
const { t } = useTranslation()
const [layout, setLayout] = useState<LayoutName>('Circular') const [layout, setLayout] = useState<LayoutName>('Circular')
const [opened, setOpened] = useState<boolean>(false) const [opened, setOpened] = useState<boolean>(false)
@@ -149,7 +152,7 @@ const LayoutsControl = () => {
size="icon" size="icon"
variant={controlButtonVariant} variant={controlButtonVariant}
onClick={() => setOpened((e: boolean) => !e)} onClick={() => setOpened((e: boolean) => !e)}
tooltip="Layout Graph" tooltip={t('graphPanel.sideBar.layoutsControl.layoutGraph')}
> >
<GripIcon /> <GripIcon />
</Button> </Button>
@@ -166,7 +169,7 @@ const LayoutsControl = () => {
key={name} key={name}
className="cursor-pointer text-xs" className="cursor-pointer text-xs"
> >
{name} {t(`graphPanel.sideBar.layoutsControl.layouts.${name}`)}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph' import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
import Text from '@/components/ui/Text' import Text from '@/components/ui/Text'
import useLightragGraph from '@/hooks/useLightragGraph' import useLightragGraph from '@/hooks/useLightragGraph'
import { useTranslation } from 'react-i18next'
/** /**
* Component that view properties of elements in graph. * Component that view properties of elements in graph.
@@ -147,21 +148,22 @@ const PropertyRow = ({
} }
const NodePropertiesView = ({ node }: { node: NodeType }) => { const NodePropertiesView = ({ node }: { node: NodeType }) => {
const { t } = useTranslation()
return ( return (
<div className="flex flex-col gap-2"> <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"> <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 <PropertyRow
name={'Labels'} name={t('graphPanel.propertiesView.node.labels')}
value={node.labels.join(', ')} value={node.labels.join(', ')}
onClick={() => { onClick={() => {
useGraphStore.getState().setSelectedNode(node.id, true) useGraphStore.getState().setSelectedNode(node.id, true)
}} }}
/> />
<PropertyRow name={'Degree'} value={node.degree} /> <PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
</div> </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"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(node.properties) {Object.keys(node.properties)
.sort() .sort()
@@ -172,7 +174,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
{node.relationships.length > 0 && ( {node.relationships.length > 0 && (
<> <>
<label className="text-md pl-1 font-bold tracking-wide text-teal-600/90"> <label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
Relationships {t('graphPanel.propertiesView.node.relationships')}
</label> </label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{node.relationships.map(({ type, id, label }) => { {node.relationships.map(({ type, id, label }) => {
@@ -195,28 +197,29 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
} }
const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => { const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
const { t } = useTranslation()
return ( return (
<div className="flex flex-col gap-2"> <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"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={'Id'} value={edge.id} /> <PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
{edge.type && <PropertyRow name={'Type'} value={edge.type} />} {edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
<PropertyRow <PropertyRow
name={'Source'} name={t('graphPanel.propertiesView.edge.source')}
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source} value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
onClick={() => { onClick={() => {
useGraphStore.getState().setSelectedNode(edge.source, true) useGraphStore.getState().setSelectedNode(edge.source, true)
}} }}
/> />
<PropertyRow <PropertyRow
name={'Target'} name={t('graphPanel.propertiesView.edge.target')}
value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target} value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
onClick={() => { onClick={() => {
useGraphStore.getState().setSelectedNode(edge.target, true) useGraphStore.getState().setSelectedNode(edge.target, true)
}} }}
/> />
</div> </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"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(edge.properties) {Object.keys(edge.properties)
.sort() .sort()

View File

@@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'
import { useBackendState } from '@/stores/state' import { useBackendState } from '@/stores/state'
import { SettingsIcon } from 'lucide-react' import { SettingsIcon } from 'lucide-react'
import { useTranslation } from "react-i18next";
/** /**
* Component that displays a checkbox with a label. * Component that displays a checkbox with a label.
@@ -204,10 +205,12 @@ export default function Settings() {
[setTempApiKey] [setTempApiKey]
) )
const { t } = useTranslation();
return ( return (
<Popover open={opened} onOpenChange={setOpened}> <Popover open={opened} onOpenChange={setOpened}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant={controlButtonVariant} tooltip="Settings" size="icon"> <Button variant={controlButtonVariant} tooltip={t("graphPanel.sideBar.settings.settings")} size="icon">
<SettingsIcon /> <SettingsIcon />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -221,7 +224,7 @@ export default function Settings() {
<LabeledCheckBox <LabeledCheckBox
checked={enableHealthCheck} checked={enableHealthCheck}
onCheckedChange={setEnableHealthCheck} onCheckedChange={setEnableHealthCheck}
label="Health Check" label={t("graphPanel.sideBar.settings.healthCheck")}
/> />
<Separator /> <Separator />
@@ -229,12 +232,12 @@ export default function Settings() {
<LabeledCheckBox <LabeledCheckBox
checked={showPropertyPanel} checked={showPropertyPanel}
onCheckedChange={setShowPropertyPanel} onCheckedChange={setShowPropertyPanel}
label="Show Property Panel" label={t("graphPanel.sideBar.settings.showPropertyPanel")}
/> />
<LabeledCheckBox <LabeledCheckBox
checked={showNodeSearchBar} checked={showNodeSearchBar}
onCheckedChange={setShowNodeSearchBar} onCheckedChange={setShowNodeSearchBar}
label="Show Search Bar" label={t("graphPanel.sideBar.settings.showSearchBar")}
/> />
<Separator /> <Separator />
@@ -242,12 +245,12 @@ export default function Settings() {
<LabeledCheckBox <LabeledCheckBox
checked={showNodeLabel} checked={showNodeLabel}
onCheckedChange={setShowNodeLabel} onCheckedChange={setShowNodeLabel}
label="Show Node Label" label={t("graphPanel.sideBar.settings.showNodeLabel")}
/> />
<LabeledCheckBox <LabeledCheckBox
checked={enableNodeDrag} checked={enableNodeDrag}
onCheckedChange={setEnableNodeDrag} onCheckedChange={setEnableNodeDrag}
label="Node Draggable" label={t("graphPanel.sideBar.settings.nodeDraggable")}
/> />
<Separator /> <Separator />
@@ -255,51 +258,50 @@ export default function Settings() {
<LabeledCheckBox <LabeledCheckBox
checked={showEdgeLabel} checked={showEdgeLabel}
onCheckedChange={setShowEdgeLabel} onCheckedChange={setShowEdgeLabel}
label="Show Edge Label" label={t("graphPanel.sideBar.settings.showEdgeLabel")}
/> />
<LabeledCheckBox <LabeledCheckBox
checked={enableHideUnselectedEdges} checked={enableHideUnselectedEdges}
onCheckedChange={setEnableHideUnselectedEdges} onCheckedChange={setEnableHideUnselectedEdges}
label="Hide Unselected Edges" label={t("graphPanel.sideBar.settings.hideUnselectedEdges")}
/> />
<LabeledCheckBox <LabeledCheckBox
checked={enableEdgeEvents} checked={enableEdgeEvents}
onCheckedChange={setEnableEdgeEvents} onCheckedChange={setEnableEdgeEvents}
label="Edge Events" label={t("graphPanel.sideBar.settings.edgeEvents")}
/> />
<Separator /> <Separator />
<LabeledNumberInput <LabeledNumberInput
label="Max Query Depth" label={t("graphPanel.sideBar.settings.maxQueryDepth")}
min={1} min={1}
value={graphQueryMaxDepth} value={graphQueryMaxDepth}
onEditFinished={setGraphQueryMaxDepth} onEditFinished={setGraphQueryMaxDepth}
/> />
<LabeledNumberInput <LabeledNumberInput
label="Minimum Degree" label={t("graphPanel.sideBar.settings.minDegree")}
min={0} min={0}
value={graphMinDegree} value={graphMinDegree}
onEditFinished={setGraphMinDegree} onEditFinished={setGraphMinDegree}
/> />
<LabeledNumberInput <LabeledNumberInput
label="Max Layout Iterations" label={t("graphPanel.sideBar.settings.maxLayoutIterations")}
min={1} min={1}
max={20} max={20}
value={graphLayoutMaxIterations} value={graphLayoutMaxIterations}
onEditFinished={setGraphLayoutMaxIterations} onEditFinished={setGraphLayoutMaxIterations}
/> />
<Separator /> <Separator />
<div className="flex flex-col gap-2"> <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()}> <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
<div className="w-0 flex-1"> <div className="w-0 flex-1">
<Input <Input
type="password" type="password"
value={tempApiKey} value={tempApiKey}
onChange={handleTempApiKeyChange} onChange={handleTempApiKeyChange}
placeholder="Enter your API key" placeholder={t("graphPanel.sideBar.settings.enterYourAPIkey")}
className="max-h-full w-full min-w-0" className="max-h-full w-full min-w-0"
autoComplete="off" autoComplete="off"
/> />
@@ -310,7 +312,7 @@ export default function Settings() {
size="sm" size="sm"
className="max-h-full shrink-0" className="max-h-full shrink-0"
> >
Save {t("graphPanel.sideBar.settings.save")}
</Button> </Button>
</form> </form>
</div> </div>

View File

@@ -1,58 +1,60 @@
import { LightragStatus } from '@/api/lightrag' import { LightragStatus } from '@/api/lightrag'
import { useTranslation } from 'react-i18next'
const StatusCard = ({ status }: { status: LightragStatus | null }) => { const StatusCard = ({ status }: { status: LightragStatus | null }) => {
const { t } = useTranslation()
if (!status) { 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 ( return (
<div className="min-w-[300px] space-y-3 text-sm"> <div className="min-w-[300px] space-y-3 text-sm">
<div className="space-y-1"> <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"> <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 className="truncate">{status.working_directory}</span>
<span>Input Directory:</span> <span>{t('graphPanel.statusCard.inputDirectory')}:</span>
<span className="truncate">{status.input_directory}</span> <span className="truncate">{status.input_directory}</span>
</div> </div>
</div> </div>
<div className="space-y-1"> <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"> <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>{status.configuration.llm_binding}</span>
<span>LLM Binding Host:</span> <span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
<span>{status.configuration.llm_binding_host}</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>{status.configuration.llm_model}</span>
<span>Max Tokens:</span> <span>{t('graphPanel.statusCard.maxTokens')}:</span>
<span>{status.configuration.max_tokens}</span> <span>{status.configuration.max_tokens}</span>
</div> </div>
</div> </div>
<div className="space-y-1"> <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"> <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>{status.configuration.embedding_binding}</span>
<span>Embedding Binding Host:</span> <span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
<span>{status.configuration.embedding_binding_host}</span> <span>{status.configuration.embedding_binding_host}</span>
<span>Embedding Model:</span> <span>{t('graphPanel.statusCard.embeddingModel')}:</span>
<span>{status.configuration.embedding_model}</span> <span>{status.configuration.embedding_model}</span>
</div> </div>
</div> </div>
<div className="space-y-1"> <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"> <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>{status.configuration.kv_storage}</span>
<span>Doc Status Storage:</span> <span>{t('graphPanel.statusCard.docStatusStorage')}:</span>
<span>{status.configuration.doc_status_storage}</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>{status.configuration.graph_storage}</span>
<span>Vector Storage:</span> <span>{t('graphPanel.statusCard.vectorStorage')}:</span>
<span>{status.configuration.vector_storage}</span> <span>{status.configuration.vector_storage}</span>
</div> </div>
</div> </div>

View File

@@ -3,8 +3,10 @@ import { useBackendState } from '@/stores/state'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import StatusCard from '@/components/graph/StatusCard' import StatusCard from '@/components/graph/StatusCard'
import { useTranslation } from 'react-i18next'
const StatusIndicator = () => { const StatusIndicator = () => {
const { t } = useTranslation()
const health = useBackendState.use.health() const health = useBackendState.use.health()
const lastCheckTime = useBackendState.use.lastCheckTime() const lastCheckTime = useBackendState.use.lastCheckTime()
const status = useBackendState.use.status() const status = useBackendState.use.status()
@@ -33,7 +35,7 @@ const StatusIndicator = () => {
)} )}
/> />
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{health ? 'Connected' : 'Disconnected'} {health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')}
</span> </span>
</div> </div>
</PopoverTrigger> </PopoverTrigger>

View File

@@ -3,12 +3,14 @@ import { useCallback } from 'react'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react' import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
import { controlButtonVariant } from '@/lib/constants' import { controlButtonVariant } from '@/lib/constants'
import { useTranslation } from "react-i18next";
/** /**
* Component that provides zoom controls for the graph viewer. * Component that provides zoom controls for the graph viewer.
*/ */
const ZoomControl = () => { const ZoomControl = () => {
const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 }) const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
const { t } = useTranslation();
const handleZoomIn = useCallback(() => zoomIn(), [zoomIn]) const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
const handleZoomOut = useCallback(() => zoomOut(), [zoomOut]) const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
@@ -16,16 +18,16 @@ const ZoomControl = () => {
return ( 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 /> <ZoomInIcon />
</Button> </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 /> <ZoomOutIcon />
</Button> </Button>
<Button <Button
variant={controlButtonVariant} variant={controlButtonVariant}
onClick={handleResetZoom} onClick={handleResetZoom}
tooltip="Reset Zoom" tooltip={t("graphPanel.sideBar.zoomControl.resetZoom")}
size="icon" size="icon"
> >
<FullscreenIcon /> <FullscreenIcon />

View File

@@ -15,18 +15,21 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
import { LoaderIcon, CopyIcon } from 'lucide-react' import { LoaderIcon, CopyIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export type MessageWithError = Message & { export type MessageWithError = Message & {
isError?: boolean isError?: boolean
} }
export const ChatMessage = ({ message }: { message: MessageWithError }) => { export const ChatMessage = ({ message }: { message: MessageWithError }) => {
const { t } = useTranslation()
const handleCopyMarkdown = useCallback(async () => { const handleCopyMarkdown = useCallback(async () => {
if (message.content) { if (message.content) {
try { try {
await navigator.clipboard.writeText(message.content) await navigator.clipboard.writeText(message.content)
} catch (err) { } catch (err) {
console.error('Failed to copy:', err) console.error(t('chat.copyError'), err)
} }
} }
}, [message]) }, [message])
@@ -57,7 +60,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => {
<Button <Button
onClick={handleCopyMarkdown} onClick={handleCopyMarkdown}
className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100" 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" variant="default"
size="icon" size="icon"
> >

View File

@@ -14,8 +14,10 @@ import {
SelectValue SelectValue
} from '@/components/ui/Select' } from '@/components/ui/Select'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useTranslation } from 'react-i18next'
export default function QuerySettings() { export default function QuerySettings() {
const { t } = useTranslation()
const querySettings = useSettingsStore((state) => state.querySettings) const querySettings = useSettingsStore((state) => state.querySettings)
const handleChange = useCallback((key: keyof QueryRequest, value: any) => { const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
@@ -25,8 +27,8 @@ export default function QuerySettings() {
return ( return (
<Card className="flex shrink-0 flex-col"> <Card className="flex shrink-0 flex-col">
<CardHeader className="px-4 pt-4 pb-2"> <CardHeader className="px-4 pt-4 pb-2">
<CardTitle>Parameters</CardTitle> <CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
<CardDescription>Configure your query parameters</CardDescription> <CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="m-0 flex grow flex-col p-0 text-xs"> <CardContent className="m-0 flex grow flex-col p-0 text-xs">
<div className="relative size-full"> <div className="relative size-full">
@@ -35,8 +37,8 @@ export default function QuerySettings() {
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="Query Mode" text={t('retrievePanel.querySettings.queryMode')}
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" tooltip={t('retrievePanel.querySettings.queryModeTooltip')}
side="left" side="left"
/> />
<Select <Select
@@ -48,11 +50,11 @@ export default function QuerySettings() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
<SelectItem value="naive">Naive</SelectItem> <SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
<SelectItem value="local">Local</SelectItem> <SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
<SelectItem value="global">Global</SelectItem> <SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
<SelectItem value="hybrid">Hybrid</SelectItem> <SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
<SelectItem value="mix">Mix</SelectItem> <SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -62,8 +64,8 @@ export default function QuerySettings() {
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="Response Format" text={t('retrievePanel.querySettings.responseFormat')}
tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points" tooltip={t('retrievePanel.querySettings.responseFormatTooltip')}
side="left" side="left"
/> />
<Select <Select
@@ -75,9 +77,9 @@ export default function QuerySettings() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
<SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem> <SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
<SelectItem value="Single Paragraph">Single Paragraph</SelectItem> <SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
<SelectItem value="Bullet Points">Bullet Points</SelectItem> <SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -87,8 +89,8 @@ export default function QuerySettings() {
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="Top K Results" text={t('retrievePanel.querySettings.topK')}
tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode" tooltip={t('retrievePanel.querySettings.topKTooltip')}
side="left" side="left"
/> />
<NumberInput <NumberInput
@@ -97,7 +99,7 @@ export default function QuerySettings() {
value={querySettings.top_k} value={querySettings.top_k}
onValueChange={(v) => handleChange('top_k', v)} onValueChange={(v) => handleChange('top_k', v)}
min={1} min={1}
placeholder="Number of results" placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
/> />
</> </>
@@ -106,8 +108,8 @@ export default function QuerySettings() {
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="Max Tokens for Text Unit" text={t('retrievePanel.querySettings.maxTokensTextUnit')}
tooltip="Maximum number of tokens allowed for each retrieved text chunk" tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
side="left" side="left"
/> />
<NumberInput <NumberInput
@@ -116,14 +118,14 @@ export default function QuerySettings() {
value={querySettings.max_token_for_text_unit} value={querySettings.max_token_for_text_unit}
onValueChange={(v) => handleChange('max_token_for_text_unit', v)} onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
min={1} min={1}
placeholder="Max tokens for text unit" placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
/> />
</> </>
<> <>
<Text <Text
text="Max Tokens for Global Context" text={t('retrievePanel.querySettings.maxTokensGlobalContext')}
tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval" tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
side="left" side="left"
/> />
<NumberInput <NumberInput
@@ -132,15 +134,15 @@ export default function QuerySettings() {
value={querySettings.max_token_for_global_context} value={querySettings.max_token_for_global_context}
onValueChange={(v) => handleChange('max_token_for_global_context', v)} onValueChange={(v) => handleChange('max_token_for_global_context', v)}
min={1} min={1}
placeholder="Max tokens for global context" placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
/> />
</> </>
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="Max Tokens for Local Context" text={t('retrievePanel.querySettings.maxTokensLocalContext')}
tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval" tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
side="left" side="left"
/> />
<NumberInput <NumberInput
@@ -149,7 +151,7 @@ export default function QuerySettings() {
value={querySettings.max_token_for_local_context} value={querySettings.max_token_for_local_context}
onValueChange={(v) => handleChange('max_token_for_local_context', v)} onValueChange={(v) => handleChange('max_token_for_local_context', v)}
min={1} min={1}
placeholder="Max tokens for local context" placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
/> />
</> </>
</> </>
@@ -158,8 +160,8 @@ export default function QuerySettings() {
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="History Turns" text={t('retrievePanel.querySettings.historyTurns')}
tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context" tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
side="left" side="left"
/> />
<NumberInput <NumberInput
@@ -170,7 +172,7 @@ export default function QuerySettings() {
value={querySettings.history_turns} value={querySettings.history_turns}
onValueChange={(v) => handleChange('history_turns', v)} onValueChange={(v) => handleChange('history_turns', v)}
min={0} min={0}
placeholder="Number of history turns" placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
/> />
</> </>
@@ -179,8 +181,8 @@ export default function QuerySettings() {
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="High-Level Keywords" text={t('retrievePanel.querySettings.hlKeywords')}
tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas" tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
side="left" side="left"
/> />
<Input <Input
@@ -194,15 +196,15 @@ export default function QuerySettings() {
.filter((k) => k !== '') .filter((k) => k !== '')
handleChange('hl_keywords', keywords) handleChange('hl_keywords', keywords)
}} }}
placeholder="Enter keywords" placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
/> />
</> </>
<> <>
<Text <Text
className="ml-1" className="ml-1"
text="Low-Level Keywords" text={t('retrievePanel.querySettings.llKeywords')}
tooltip="List of low-level keywords to refine retrieval focus. Separate with commas" tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
side="left" side="left"
/> />
<Input <Input
@@ -216,7 +218,7 @@ export default function QuerySettings() {
.filter((k) => k !== '') .filter((k) => k !== '')
handleChange('ll_keywords', keywords) 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"> <div className="flex items-center gap-2">
<Text <Text
className="ml-1" className="ml-1"
text="Only Need Context" text={t('retrievePanel.querySettings.onlyNeedContext')}
tooltip="If True, only returns the retrieved context without generating a response" tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
side="left" side="left"
/> />
<div className="grow" /> <div className="grow" />
@@ -242,8 +244,8 @@ export default function QuerySettings() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <Text
className="ml-1" className="ml-1"
text="Only Need Prompt" text={t('retrievePanel.querySettings.onlyNeedPrompt')}
tooltip="If True, only returns the generated prompt without producing a response" tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
side="left" side="left"
/> />
<div className="grow" /> <div className="grow" />
@@ -258,8 +260,8 @@ export default function QuerySettings() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <Text
className="ml-1" className="ml-1"
text="Stream Response" text={t('retrievePanel.querySettings.streamResponse')}
tooltip="If True, enables streaming output for real-time responses" tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
side="left" side="left"
/> />
<div className="grow" /> <div className="grow" />

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { import {
Table, Table,
@@ -22,6 +23,7 @@ import { useBackendState } from '@/stores/state'
import { RefreshCwIcon } from 'lucide-react' import { RefreshCwIcon } from 'lucide-react'
export default function DocumentManager() { export default function DocumentManager() {
const { t } = useTranslation()
const health = useBackendState.use.health() const health = useBackendState.use.health()
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null) const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
@@ -44,7 +46,7 @@ export default function DocumentManager() {
setDocs(null) setDocs(null)
} }
} catch (err) { } catch (err) {
toast.error('Failed to load documents\n' + errorMessage(err)) toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
} }
}, [setDocs]) }, [setDocs])
@@ -57,7 +59,7 @@ export default function DocumentManager() {
const { status } = await scanNewDocuments() const { status } = await scanNewDocuments()
toast.message(status) toast.message(status)
} catch (err) { } 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 { try {
await fetchDocuments() await fetchDocuments()
} catch (err) { } catch (err) {
toast.error('Failed to get scan progress\n' + errorMessage(err)) toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
} }
}, 5000) }, 5000)
return () => clearInterval(interval) return () => clearInterval(interval)
@@ -78,7 +80,7 @@ export default function DocumentManager() {
return ( return (
<Card className="!size-full !rounded-none !border-none"> <Card className="!size-full !rounded-none !border-none">
<CardHeader> <CardHeader>
<CardTitle className="text-lg">Document Management</CardTitle> <CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex gap-2"> <div className="flex gap-2">
@@ -86,10 +88,10 @@ export default function DocumentManager() {
variant="outline" variant="outline"
onClick={scanDocuments} onClick={scanDocuments}
side="bottom" side="bottom"
tooltip="Scan documents" tooltip={t('documentPanel.documentManager.scanTooltip')}
size="sm" size="sm"
> >
<RefreshCwIcon /> Scan <RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
</Button> </Button>
<div className="flex-1" /> <div className="flex-1" />
<ClearDocumentsDialog /> <ClearDocumentsDialog />
@@ -98,29 +100,29 @@ export default function DocumentManager() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Uploaded documents</CardTitle> <CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
<CardDescription>view the uploaded documents here</CardDescription> <CardDescription>{t('documentPanel.documentManager.uploadedDescription')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{!docs && ( {!docs && (
<EmptyCard <EmptyCard
title="No documents uploaded" title={t('documentPanel.documentManager.emptyTitle')}
description="upload documents to see them here" description={t('documentPanel.documentManager.emptyDescription')}
/> />
)} )}
{docs && ( {docs && (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>ID</TableHead> <TableHead>{t('documentPanel.documentManager.columns.id')}</TableHead>
<TableHead>Summary</TableHead> <TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
<TableHead>Status</TableHead> <TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
<TableHead>Length</TableHead> <TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
<TableHead>Chunks</TableHead> <TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
<TableHead>Created</TableHead> <TableHead>{t('documentPanel.documentManager.columns.created')}</TableHead>
<TableHead>Updated</TableHead> <TableHead>{t('documentPanel.documentManager.columns.updated')}</TableHead>
<TableHead>Metadata</TableHead> <TableHead>{t('documentPanel.documentManager.columns.metadata')}</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody className="text-sm"> <TableBody className="text-sm">
@@ -137,13 +139,13 @@ export default function DocumentManager() {
</TableCell> </TableCell>
<TableCell> <TableCell>
{status === 'processed' && ( {status === 'processed' && (
<span className="text-green-600">Completed</span> <span className="text-green-600">{t('documentPanel.documentManager.status.completed')}</span>
)} )}
{status === 'processing' && ( {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 === 'pending' && <span className="text-yellow-600">{t('documentPanel.documentManager.status.pending')}</span>}
{status === 'failed' && <span className="text-red-600">Failed</span>} {status === 'failed' && <span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>}
{doc.error && ( {doc.error && (
<span className="ml-2 text-red-500" title={doc.error}> <span className="ml-2 text-red-500" title={doc.error}>

View File

@@ -8,8 +8,10 @@ import { useDebounce } from '@/hooks/useDebounce'
import QuerySettings from '@/components/retrieval/QuerySettings' import QuerySettings from '@/components/retrieval/QuerySettings'
import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage' import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
import { EraserIcon, SendIcon } from 'lucide-react' import { EraserIcon, SendIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function RetrievalTesting() { export default function RetrievalTesting() {
const { t } = useTranslation()
const [messages, setMessages] = useState<MessageWithError[]>( const [messages, setMessages] = useState<MessageWithError[]>(
() => useSettingsStore.getState().retrievalHistory || [] () => useSettingsStore.getState().retrievalHistory || []
) )
@@ -89,7 +91,7 @@ export default function RetrievalTesting() {
} }
} catch (err) { } catch (err) {
// Handle error // Handle error
updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`, true) updateAssistantMessage(`${t('retrievePanel.retrieval.error')}\n${errorMessage(err)}`, true)
} finally { } finally {
// Clear loading and add messages to state // Clear loading and add messages to state
setIsLoading(false) setIsLoading(false)
@@ -98,7 +100,7 @@ export default function RetrievalTesting() {
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage]) .setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
} }
}, },
[inputValue, isLoading, messages, setMessages] [inputValue, isLoading, messages, setMessages, t]
) )
const debouncedMessages = useDebounce(messages, 100) 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"> <div className="flex min-h-0 flex-1 flex-col gap-2">
{messages.length === 0 ? ( {messages.length === 0 ? (
<div className="text-muted-foreground flex h-full items-center justify-center text-lg"> <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> </div>
) : ( ) : (
messages.map((message, idx) => ( messages.map((message, idx) => (
@@ -143,18 +145,18 @@ export default function RetrievalTesting() {
size="sm" size="sm"
> >
<EraserIcon /> <EraserIcon />
Clear {t('retrievePanel.retrieval.clear')}
</Button> </Button>
<Input <Input
className="flex-1" className="flex-1"
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
placeholder="Type your query..." placeholder={t('retrievePanel.retrieval.placeholder')}
disabled={isLoading} disabled={isLoading}
/> />
<Button type="submit" variant="default" disabled={isLoading} size="sm"> <Button type="submit" variant="default" disabled={isLoading} size="sm">
<SendIcon /> <SendIcon />
Send {t('retrievePanel.retrieval.send')}
</Button> </Button>
</form> </form>
</div> </div>

View File

@@ -4,6 +4,7 @@ import ThemeToggle from '@/components/ThemeToggle'
import { TabsList, TabsTrigger } from '@/components/ui/Tabs' import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { ZapIcon, GithubIcon } from 'lucide-react' import { ZapIcon, GithubIcon } from 'lucide-react'
@@ -29,21 +30,22 @@ function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
function TabsNavigation() { function TabsNavigation() {
const currentTab = useSettingsStore.use.currentTab() const currentTab = useSettingsStore.use.currentTab()
const { t } = useTranslation()
return ( return (
<div className="flex h-8 self-center"> <div className="flex h-8 self-center">
<TabsList className="h-full gap-2"> <TabsList className="h-full gap-2">
<NavigationTab value="documents" currentTab={currentTab}> <NavigationTab value="documents" currentTab={currentTab}>
Documents {t('header.documents')}
</NavigationTab> </NavigationTab>
<NavigationTab value="knowledge-graph" currentTab={currentTab}> <NavigationTab value="knowledge-graph" currentTab={currentTab}>
Knowledge Graph {t('header.knowledgeGraph')}
</NavigationTab> </NavigationTab>
<NavigationTab value="retrieval" currentTab={currentTab}> <NavigationTab value="retrieval" currentTab={currentTab}>
Retrieval {t('header.retrieval')}
</NavigationTab> </NavigationTab>
<NavigationTab value="api" currentTab={currentTab}> <NavigationTab value="api" currentTab={currentTab}>
API {t('header.api')}
</NavigationTab> </NavigationTab>
</TabsList> </TabsList>
</div> </div>
@@ -51,6 +53,7 @@ function TabsNavigation() {
} }
export default function SiteHeader() { export default function SiteHeader() {
const { t } = useTranslation()
return ( 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"> <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"> <a href="/" className="mr-6 flex items-center gap-2">
@@ -64,7 +67,7 @@ export default function SiteHeader() {
</div> </div>
<nav className="flex items-center"> <nav className="flex items-center">
<Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository"> <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer"> <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
<GithubIcon className="size-4" aria-hidden="true" /> <GithubIcon className="size-4" aria-hidden="true" />
</a> </a>

View File

@@ -0,0 +1,21 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "./locales/en.json";
import zh from "./locales/zh.json";
i18n
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
zh: { translation: zh }
},
lng: "en", // default
fallbackLng: "en",
interpolation: {
escapeValue: false
}
});
export default i18n;

View File

@@ -0,0 +1,234 @@
{
"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",
"apiKey": "API Key",
"enterYourAPIkey": "Enter your API key",
"save": "Save"
},
"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"
}
}
}

View File

@@ -0,0 +1,235 @@
{
"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": "最大布局迭代次数",
"apiKey": "API 密钥",
"enterYourAPIkey": "输入您的 API 密钥",
"save": "保存"
},
"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": "最大 Token 数",
"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• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索",
"queryModeOptions": {
"naive": "朴素",
"local": "本地",
"global": "全局",
"hybrid": "混合",
"mix": "综合"
},
"responseFormat": "响应格式",
"responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号",
"responseFormatOptions": {
"multipleParagraphs": "多个段落",
"singleParagraph": "单个段落",
"bulletPoints": "项目符号"
},
"topK": "Top K 结果数",
"topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系",
"topKPlaceholder": "结果数",
"maxTokensTextUnit": "文本单元最大 Token 数",
"maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数",
"maxTokensGlobalContext": "全局上下文最大 Token 数",
"maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
"maxTokensLocalContext": "本地上下文最大 Token 数",
"maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
"historyTurns": "历史轮次",
"historyTurnsTooltip": "在响应上下文中考虑的完整对话轮次(用户-助手对)",
"historyTurnsPlaceholder": "历史轮次的数量",
"hlKeywords": "高级关键词",
"hlKeywordsTooltip": "检索时优先考虑的高级关键词。请用逗号分隔",
"hlkeywordsPlaceHolder": "输入关键词",
"llKeywords": "低级关键词",
"llKeywordsTooltip": "用于优化检索焦点的低级关键词。请用逗号分隔",
"onlyNeedContext": "仅需要上下文",
"onlyNeedContextTooltip": "如果为 True则仅返回检索到的上下文而不会生成回复",
"onlyNeedPrompt": "仅需要提示",
"onlyNeedPromptTooltip": "如果为 True则仅返回生成的提示而不会生成回复",
"streamResponse": "流式响应",
"streamResponseTooltip": "如果为 True则启用流式输出以获得实时响应"
}
}
}

View File

@@ -2,6 +2,8 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import App from './App.tsx' import App from './App.tsx'
import "./i18n";
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>