diff --git a/lightrag/base.py b/lightrag/base.py index c84c7c62..86566787 100644 --- a/lightrag/base.py +++ b/lightrag/base.py @@ -127,6 +127,30 @@ class BaseVectorStorage(StorageNameSpace, ABC): async def delete_entity_relation(self, entity_name: str) -> None: """Delete relations for a given entity.""" + @abstractmethod + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + pass + + @abstractmethod + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + pass + @dataclass class BaseKVStorage(StorageNameSpace, ABC): diff --git a/lightrag/kg/chroma_impl.py b/lightrag/kg/chroma_impl.py index 6b521180..84d43326 100644 --- a/lightrag/kg/chroma_impl.py +++ b/lightrag/kg/chroma_impl.py @@ -156,7 +156,9 @@ class ChromaVectorDBStorage(BaseVectorStorage): logger.error(f"Error during ChromaDB upsert: {str(e)}") raise - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: try: embedding = await self.embedding_func([query]) @@ -269,3 +271,67 @@ class ChromaVectorDBStorage(BaseVectorStorage): except Exception as e: logger.error(f"Error during prefix search in ChromaDB: {str(e)}") raise + + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + try: + # Query the collection for a single vector by ID + result = self._collection.get( + ids=[id], include=["metadatas", "embeddings", "documents"] + ) + + if not result or not result["ids"] or len(result["ids"]) == 0: + return None + + # Format the result to match the expected structure + return { + "id": result["ids"][0], + "vector": result["embeddings"][0], + "content": result["documents"][0], + **result["metadatas"][0], + } + except Exception as e: + logger.error(f"Error retrieving vector data for ID {id}: {e}") + return None + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + try: + # Query the collection for multiple vectors by IDs + result = self._collection.get( + ids=ids, include=["metadatas", "embeddings", "documents"] + ) + + if not result or not result["ids"] or len(result["ids"]) == 0: + return [] + + # Format the results to match the expected structure + return [ + { + "id": result["ids"][i], + "vector": result["embeddings"][i], + "content": result["documents"][i], + **result["metadatas"][i], + } + for i in range(len(result["ids"])) + ] + except Exception as e: + logger.error(f"Error retrieving vector data for IDs {ids}: {e}") + return [] diff --git a/lightrag/kg/faiss_impl.py b/lightrag/kg/faiss_impl.py index ab036e6f..57b0cae0 100644 --- a/lightrag/kg/faiss_impl.py +++ b/lightrag/kg/faiss_impl.py @@ -171,7 +171,9 @@ class FaissVectorDBStorage(BaseVectorStorage): logger.info(f"Upserted {len(list_data)} vectors into Faiss index.") return [m["__id__"] for m in list_data] - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: """ Search by a textual query; returns top_k results with their metadata + similarity distance. """ @@ -392,3 +394,46 @@ class FaissVectorDBStorage(BaseVectorStorage): logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'") return matching_records + + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + # Find the Faiss internal ID for the custom ID + fid = self._find_faiss_id_by_custom_id(id) + if fid is None: + return None + + # Get the metadata for the found ID + metadata = self._id_to_meta.get(fid, {}) + if not metadata: + return None + + return {**metadata, "id": metadata.get("__id__")} + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + results = [] + for id in ids: + fid = self._find_faiss_id_by_custom_id(id) + if fid is not None: + metadata = self._id_to_meta.get(fid, {}) + if metadata: + results.append({**metadata, "id": metadata.get("__id__")}) + + return results diff --git a/lightrag/kg/milvus_impl.py b/lightrag/kg/milvus_impl.py index f3a6fcc4..4b4577ca 100644 --- a/lightrag/kg/milvus_impl.py +++ b/lightrag/kg/milvus_impl.py @@ -101,7 +101,9 @@ class MilvusVectorDBStorage(BaseVectorStorage): results = self._client.upsert(collection_name=self.namespace, data=list_data) return results - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: embedding = await self.embedding_func([query]) results = self._client.search( collection_name=self.namespace, @@ -231,3 +233,57 @@ class MilvusVectorDBStorage(BaseVectorStorage): except Exception as e: logger.error(f"Error searching for records with prefix '{prefix}': {e}") return [] + + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + try: + # Query Milvus for a specific ID + result = self._client.query( + collection_name=self.namespace, + filter=f'id == "{id}"', + output_fields=list(self.meta_fields) + ["id"], + ) + + if not result or len(result) == 0: + return None + + return result[0] + except Exception as e: + logger.error(f"Error retrieving vector data for ID {id}: {e}") + return None + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + try: + # Prepare the ID filter expression + id_list = '", "'.join(ids) + filter_expr = f'id in ["{id_list}"]' + + # Query Milvus with the filter + result = self._client.query( + collection_name=self.namespace, + filter=filter_expr, + output_fields=list(self.meta_fields) + ["id"], + ) + + return result or [] + except Exception as e: + logger.error(f"Error retrieving vector data for IDs {ids}: {e}") + return [] diff --git a/lightrag/kg/mongo_impl.py b/lightrag/kg/mongo_impl.py index f2ab6ae0..7d43e4f4 100644 --- a/lightrag/kg/mongo_impl.py +++ b/lightrag/kg/mongo_impl.py @@ -938,7 +938,9 @@ class MongoVectorDBStorage(BaseVectorStorage): return list_data - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: """Queries the vector database using Atlas Vector Search.""" # Generate the embedding embedding = await self.embedding_func([query]) @@ -1071,6 +1073,59 @@ class MongoVectorDBStorage(BaseVectorStorage): logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}") return [] + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + try: + # Search for the specific ID in MongoDB + result = await self._data.find_one({"_id": id}) + if result: + # Format the result to include id field expected by API + result_dict = dict(result) + if "_id" in result_dict and "id" not in result_dict: + result_dict["id"] = result_dict["_id"] + return result_dict + return None + except Exception as e: + logger.error(f"Error retrieving vector data for ID {id}: {e}") + return None + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + try: + # Query MongoDB for multiple IDs + cursor = self._data.find({"_id": {"$in": ids}}) + results = await cursor.to_list(length=None) + + # Format results to include id field expected by API + formatted_results = [] + for result in results: + result_dict = dict(result) + if "_id" in result_dict and "id" not in result_dict: + result_dict["id"] = result_dict["_id"] + formatted_results.append(result_dict) + + return formatted_results + except Exception as e: + logger.error(f"Error retrieving vector data for IDs {ids}: {e}") + return [] + async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str): collection_names = await db.list_collection_names() diff --git a/lightrag/kg/nano_vector_db_impl.py b/lightrag/kg/nano_vector_db_impl.py index 07ccd566..4f739091 100644 --- a/lightrag/kg/nano_vector_db_impl.py +++ b/lightrag/kg/nano_vector_db_impl.py @@ -120,7 +120,9 @@ class NanoVectorDBStorage(BaseVectorStorage): f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}" ) - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: # Execute embedding outside of lock to avoid long lock times embedding = await self.embedding_func([query]) embedding = embedding[0] @@ -256,3 +258,33 @@ class NanoVectorDBStorage(BaseVectorStorage): logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'") return matching_records + + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + client = await self._get_client() + result = client.get([id]) + if result: + return result[0] + return None + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + client = await self._get_client() + return client.get(ids) diff --git a/lightrag/kg/oracle_impl.py b/lightrag/kg/oracle_impl.py index eda3ca63..c42f0f76 100644 --- a/lightrag/kg/oracle_impl.py +++ b/lightrag/kg/oracle_impl.py @@ -417,7 +417,9 @@ class OracleVectorDBStorage(BaseVectorStorage): self.db = None #################### query method ############### - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: embeddings = await self.embedding_func([query]) embedding = embeddings[0] # 转换精度 @@ -529,6 +531,80 @@ class OracleVectorDBStorage(BaseVectorStorage): logger.error(f"Error searching records with prefix '{prefix}': {e}") return [] + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + try: + # Determine the table name based on namespace + table_name = namespace_to_table_name(self.namespace) + if not table_name: + logger.error(f"Unknown namespace for ID lookup: {self.namespace}") + return None + + # Create the appropriate ID field name based on namespace + id_field = "entity_id" if "NODES" in table_name else "relation_id" + if "CHUNKS" in table_name: + id_field = "chunk_id" + + # Prepare and execute the query + query = f""" + SELECT * FROM {table_name} + WHERE {id_field} = :id AND workspace = :workspace + """ + params = {"id": id, "workspace": self.db.workspace} + + result = await self.db.query(query, params) + return result + except Exception as e: + logger.error(f"Error retrieving vector data for ID {id}: {e}") + return None + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + try: + # Determine the table name based on namespace + table_name = namespace_to_table_name(self.namespace) + if not table_name: + logger.error(f"Unknown namespace for IDs lookup: {self.namespace}") + return [] + + # Create the appropriate ID field name based on namespace + id_field = "entity_id" if "NODES" in table_name else "relation_id" + if "CHUNKS" in table_name: + id_field = "chunk_id" + + # Format the list of IDs for SQL IN clause + ids_list = ", ".join([f"'{id}'" for id in ids]) + + # Prepare and execute the query + query = f""" + SELECT * FROM {table_name} + WHERE {id_field} IN ({ids_list}) AND workspace = :workspace + """ + params = {"workspace": self.db.workspace} + + results = await self.db.query(query, params, multirows=True) + return results or [] + except Exception as e: + logger.error(f"Error retrieving vector data for IDs {ids}: {e}") + return [] + @final @dataclass diff --git a/lightrag/kg/postgres_impl.py b/lightrag/kg/postgres_impl.py index 1d525bdb..49d462f6 100644 --- a/lightrag/kg/postgres_impl.py +++ b/lightrag/kg/postgres_impl.py @@ -621,6 +621,60 @@ class PGVectorStorage(BaseVectorStorage): logger.error(f"Error during prefix search for '{prefix}': {e}") return [] + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + table_name = namespace_to_table_name(self.namespace) + if not table_name: + logger.error(f"Unknown namespace for ID lookup: {self.namespace}") + return None + + query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id=$2" + params = {"workspace": self.db.workspace, "id": id} + + try: + result = await self.db.query(query, params) + if result: + return dict(result) + return None + except Exception as e: + logger.error(f"Error retrieving vector data for ID {id}: {e}") + return None + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + table_name = namespace_to_table_name(self.namespace) + if not table_name: + logger.error(f"Unknown namespace for IDs lookup: {self.namespace}") + return [] + + ids_str = ",".join([f"'{id}'" for id in ids]) + query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id IN ({ids_str})" + params = {"workspace": self.db.workspace} + + try: + results = await self.db.query(query, params, multirows=True) + return [dict(record) for record in results] + except Exception as e: + logger.error(f"Error retrieving vector data for IDs {ids}: {e}") + return [] + @final @dataclass diff --git a/lightrag/kg/qdrant_impl.py b/lightrag/kg/qdrant_impl.py index 53a59c2f..e32c4335 100644 --- a/lightrag/kg/qdrant_impl.py +++ b/lightrag/kg/qdrant_impl.py @@ -123,7 +123,9 @@ class QdrantVectorDBStorage(BaseVectorStorage): ) return results - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: embedding = await self.embedding_func([query]) results = self._client.search( collection_name=self.namespace, diff --git a/lightrag/kg/tidb_impl.py b/lightrag/kg/tidb_impl.py index 7af9b48a..0982c914 100644 --- a/lightrag/kg/tidb_impl.py +++ b/lightrag/kg/tidb_impl.py @@ -306,7 +306,9 @@ class TiDBVectorDBStorage(BaseVectorStorage): await ClientManager.release_client(self.db) self.db = None - async def query(self, query: str, top_k: int) -> list[dict[str, Any]]: + async def query( + self, query: str, top_k: int, ids: list[str] | None = None + ) -> list[dict[str, Any]]: """Search from tidb vector""" embeddings = await self.embedding_func([query]) embedding = embeddings[0] @@ -463,6 +465,100 @@ class TiDBVectorDBStorage(BaseVectorStorage): logger.error(f"Error searching records with prefix '{prefix}': {e}") return [] + async def get_by_id(self, id: str) -> dict[str, Any] | None: + """Get vector data by its ID + + Args: + id: The unique identifier of the vector + + Returns: + The vector data if found, or None if not found + """ + try: + # Determine which table to query based on namespace + if self.namespace == NameSpace.VECTOR_STORE_ENTITIES: + sql_template = """ + SELECT entity_id as id, name as entity_name, entity_type, description, content + FROM LIGHTRAG_GRAPH_NODES + WHERE entity_id = :entity_id AND workspace = :workspace + """ + params = {"entity_id": id, "workspace": self.db.workspace} + elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS: + sql_template = """ + SELECT relation_id as id, source_name as src_id, target_name as tgt_id, + keywords, description, content + FROM LIGHTRAG_GRAPH_EDGES + WHERE relation_id = :relation_id AND workspace = :workspace + """ + params = {"relation_id": id, "workspace": self.db.workspace} + elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS: + sql_template = """ + SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id + FROM LIGHTRAG_DOC_CHUNKS + WHERE chunk_id = :chunk_id AND workspace = :workspace + """ + params = {"chunk_id": id, "workspace": self.db.workspace} + else: + logger.warning( + f"Namespace {self.namespace} not supported for get_by_id" + ) + return None + + result = await self.db.query(sql_template, params=params) + return result + except Exception as e: + logger.error(f"Error retrieving vector data for ID {id}: {e}") + return None + + async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]: + """Get multiple vector data by their IDs + + Args: + ids: List of unique identifiers + + Returns: + List of vector data objects that were found + """ + if not ids: + return [] + + try: + # Format IDs for SQL IN clause + ids_str = ", ".join([f"'{id}'" for id in ids]) + + # Determine which table to query based on namespace + if self.namespace == NameSpace.VECTOR_STORE_ENTITIES: + sql_template = f""" + SELECT entity_id as id, name as entity_name, entity_type, description, content + FROM LIGHTRAG_GRAPH_NODES + WHERE entity_id IN ({ids_str}) AND workspace = :workspace + """ + elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS: + sql_template = f""" + SELECT relation_id as id, source_name as src_id, target_name as tgt_id, + keywords, description, content + FROM LIGHTRAG_GRAPH_EDGES + WHERE relation_id IN ({ids_str}) AND workspace = :workspace + """ + elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS: + sql_template = f""" + SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id + FROM LIGHTRAG_DOC_CHUNKS + WHERE chunk_id IN ({ids_str}) AND workspace = :workspace + """ + else: + logger.warning( + f"Namespace {self.namespace} not supported for get_by_ids" + ) + return [] + + params = {"workspace": self.db.workspace} + results = await self.db.query(sql_template, params=params, multirows=True) + return results if results else [] + except Exception as e: + logger.error(f"Error retrieving vector data for IDs {ids}: {e}") + return [] + @final @dataclass diff --git a/lightrag/lightrag.py b/lightrag/lightrag.py index a3dc92dc..3a5e4e84 100644 --- a/lightrag/lightrag.py +++ b/lightrag/lightrag.py @@ -1710,19 +1710,7 @@ class LightRAG: async def get_entity_info( self, entity_name: str, include_vector_data: bool = False ) -> dict[str, str | None | dict[str, str]]: - """Get detailed information of an entity - - Args: - entity_name: Entity name (no need for quotes) - include_vector_data: Whether to include data from the vector database - - Returns: - dict: A dictionary containing entity information, including: - - entity_name: Entity name - - source_id: Source document ID - - graph_data: Complete node data from the graph database - - vector_data: (optional) Data from the vector database - """ + """Get detailed information of an entity""" # Get information from the graph node_data = await self.chunk_entity_relation_graph.get_node(entity_name) @@ -1737,29 +1725,15 @@ class LightRAG: # Optional: Get vector database information if include_vector_data: entity_id = compute_mdhash_id(entity_name, prefix="ent-") - vector_data = self.entities_vdb._client.get([entity_id]) - result["vector_data"] = vector_data[0] if vector_data else None + vector_data = await self.entities_vdb.get_by_id(entity_id) + result["vector_data"] = vector_data return result async def get_relation_info( self, src_entity: str, tgt_entity: str, include_vector_data: bool = False ) -> dict[str, str | None | dict[str, str]]: - """Get detailed information of a relationship - - Args: - src_entity: Source entity name (no need for quotes) - tgt_entity: Target entity name (no need for quotes) - include_vector_data: Whether to include data from the vector database - - Returns: - dict: A dictionary containing relationship information, including: - - src_entity: Source entity name - - tgt_entity: Target entity name - - source_id: Source document ID - - graph_data: Complete edge data from the graph database - - vector_data: (optional) Data from the vector database - """ + """Get detailed information of a relationship""" # Get information from the graph edge_data = await self.chunk_entity_relation_graph.get_edge( @@ -1777,8 +1751,8 @@ class LightRAG: # Optional: Get vector database information if include_vector_data: rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-") - vector_data = self.relationships_vdb._client.get([rel_id]) - result["vector_data"] = vector_data[0] if vector_data else None + vector_data = await self.relationships_vdb.get_by_id(rel_id) + result["vector_data"] = vector_data return result diff --git a/lightrag_webui/bun.lock b/lightrag_webui/bun.lock index 6157e38c..d3f75f5c 100644 --- a/lightrag_webui/bun.lock +++ b/lightrag_webui/bun.lock @@ -34,11 +34,13 @@ "cmdk": "^1.0.4", "graphology": "^0.26.0", "graphology-generators": "^0.11.2", + "i18next": "^24.2.2", "lucide-react": "^0.475.0", "minisearch": "^7.1.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-dropzone": "^14.3.6", + "react-i18next": "^15.4.1", "react-markdown": "^9.1.0", "react-number-format": "^5.4.3", "react-syntax-highlighter": "^15.6.1", @@ -60,6 +62,7 @@ "@types/node": "^22.13.5", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", + "@types/react-i18next": "^8.1.0", "@types/react-syntax-highlighter": "^15.5.13", "@types/seedrandom": "^3.0.8", "@vitejs/plugin-react-swc": "^3.8.0", @@ -441,6 +444,8 @@ "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], + "@types/react-i18next": ["@types/react-i18next@8.1.0", "", { "dependencies": { "react-i18next": "*" } }, "sha512-d4xhcjX5b3roNMObRNMfb1HinHQlQLPo8xlDj60dnHeeAw2bBymR2cy/l1giJpHzo/ZFgSvgVUvIWr4kCrenCg=="], + "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="], "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], @@ -765,8 +770,12 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "i18next": ["i18next@24.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -1093,6 +1102,8 @@ "react-dropzone": ["react-dropzone@14.3.6", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-U792j+x0rcwH/U/Slv/OBNU/LGFYbDLHKKiJoPhNaOianayZevCt4Y5S0CraPssH/6/wT6xhKDfzdXUgCBS0HQ=="], + "react-i18next": ["react-i18next@15.4.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0" } }, "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-markdown": ["react-markdown@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw=="], @@ -1271,6 +1282,8 @@ "vite": ["vite@6.1.1", "", { "dependencies": { "esbuild": "^0.24.2", "postcss": "^8.5.2", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA=="], + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], diff --git a/lightrag_webui/package.json b/lightrag_webui/package.json index 578ee36f..8a10d1e6 100644 --- a/lightrag_webui/package.json +++ b/lightrag_webui/package.json @@ -43,11 +43,13 @@ "cmdk": "^1.0.4", "graphology": "^0.26.0", "graphology-generators": "^0.11.2", + "i18next": "^24.2.2", "lucide-react": "^0.475.0", "minisearch": "^7.1.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-dropzone": "^14.3.6", + "react-i18next": "^15.4.1", "react-markdown": "^9.1.0", "react-number-format": "^5.4.3", "react-syntax-highlighter": "^15.6.1", @@ -69,6 +71,7 @@ "@types/node": "^22.13.5", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", + "@types/react-i18next": "^8.1.0", "@types/react-syntax-highlighter": "^15.5.13", "@types/seedrandom": "^3.0.8", "@vitejs/plugin-react-swc": "^3.8.0", diff --git a/lightrag_webui/src/components/ThemeToggle.tsx b/lightrag_webui/src/components/ThemeToggle.tsx index 8e92d862..ff333ff0 100644 --- a/lightrag_webui/src/components/ThemeToggle.tsx +++ b/lightrag_webui/src/components/ThemeToggle.tsx @@ -3,6 +3,7 @@ import useTheme from '@/hooks/useTheme' import { MoonIcon, SunIcon } from 'lucide-react' import { useCallback } from 'react' import { controlButtonVariant } from '@/lib/constants' +import { useTranslation } from 'react-i18next' /** * Component that toggles the theme between light and dark. @@ -11,13 +12,14 @@ export default function ThemeToggle() { const { theme, setTheme } = useTheme() const setLight = useCallback(() => setTheme('light'), [setTheme]) const setDark = useCallback(() => setTheme('dark'), [setTheme]) + const { t } = useTranslation() if (theme === 'dark') { return ( e.preventDefault()}> - Clear documents - Do you really want to clear all documents? + {t('documentPanel.clearDocuments.title')} + {t('documentPanel.clearDocuments.confirm')} diff --git a/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx index 7149eb28..7f17393c 100644 --- a/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx +++ b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx @@ -14,8 +14,10 @@ import { errorMessage } from '@/lib/utils' import { uploadDocument } from '@/api/lightrag' import { UploadIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' export default function UploadDocumentsDialog() { + const { t } = useTranslation() const [open, setOpen] = useState(false) const [isUploading, setIsUploading] = useState(false) const [progresses, setProgresses] = useState>({}) @@ -29,24 +31,24 @@ export default function UploadDocumentsDialog() { filesToUpload.map(async (file) => { try { const result = await uploadDocument(file, (percentCompleted: number) => { - console.debug(`Uploading ${file.name}: ${percentCompleted}%`) + console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted })) setProgresses((pre) => ({ ...pre, [file.name]: percentCompleted })) }) if (result.status === 'success') { - toast.success(`Upload Success:\n${file.name} uploaded successfully`) + toast.success(t('documentPanel.uploadDocuments.success', { name: file.name })) } else { - toast.error(`Upload Failed:\n${file.name}\n${result.message}`) + toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message })) } } catch (err) { - toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`) + toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) })) } }) ) } catch (err) { - toast.error('Upload Failed\n' + errorMessage(err)) + toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) })) } finally { setIsUploading(false) // setOpen(false) @@ -66,21 +68,21 @@ export default function UploadDocumentsDialog() { }} > - e.preventDefault()}> - Upload documents + {t('documentPanel.uploadDocuments.title')} - Drag and drop your documents here or click to browse. + {t('documentPanel.uploadDocuments.description')} { const { isFullScreen, toggle } = useFullScreen() + const { t } = useTranslation() return ( <> {isFullScreen ? ( - ) : ( - )} diff --git a/lightrag_webui/src/components/graph/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx index f2f50ea8..259147a6 100644 --- a/lightrag_webui/src/components/graph/GraphLabels.tsx +++ b/lightrag_webui/src/components/graph/GraphLabels.tsx @@ -4,8 +4,10 @@ import { useSettingsStore } from '@/stores/settings' import { useGraphStore } from '@/stores/graph' import { labelListLimit } from '@/lib/constants' import MiniSearch from 'minisearch' +import { useTranslation } from 'react-i18next' const GraphLabels = () => { + const { t } = useTranslation() const label = useSettingsStore.use.queryLabel() const graphLabels = useGraphStore.use.graphLabels() @@ -45,7 +47,7 @@ const GraphLabels = () => { return result.length <= labelListLimit ? result - : [...result.slice(0, labelListLimit), `And ${result.length - labelListLimit} others`] + : [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })] }, [getSearchEngine] ) @@ -68,14 +70,14 @@ const GraphLabels = () => { className="ml-2" triggerClassName="max-h-8" searchInputClassName="max-h-8" - triggerTooltip="Select query label" + triggerTooltip={t('graphPanel.graphLabels.selectTooltip')} fetcher={fetchData} renderOption={(item) =>
{item}
} getOptionValue={(item) => item} getDisplayValue={(item) =>
{item}
} notFound={
No labels found
} - label="Label" - placeholder="Search labels..." + label={t('graphPanel.graphLabels.label')} + placeholder={t('graphPanel.graphLabels.placeholder')} value={label !== null ? label : ''} onChange={setQueryLabel} clearable={false} // Prevent clearing value on reselect diff --git a/lightrag_webui/src/components/graph/GraphSearch.tsx b/lightrag_webui/src/components/graph/GraphSearch.tsx index 3edc3ede..bbb8cb5b 100644 --- a/lightrag_webui/src/components/graph/GraphSearch.tsx +++ b/lightrag_webui/src/components/graph/GraphSearch.tsx @@ -9,6 +9,7 @@ import { AsyncSearch } from '@/components/ui/AsyncSearch' import { searchResultLimit } from '@/lib/constants' import { useGraphStore } from '@/stores/graph' import MiniSearch from 'minisearch' +import { useTranslation } from 'react-i18next' interface OptionItem { id: string @@ -44,6 +45,7 @@ export const GraphSearchInput = ({ onFocus?: GraphSearchInputProps['onFocus'] value?: GraphSearchInputProps['value'] }) => { + const { t } = useTranslation() const graph = useGraphStore.use.sigmaGraph() const searchEngine = useMemo(() => { @@ -97,7 +99,7 @@ export const GraphSearchInput = ({ { type: 'message', id: messageId, - message: `And ${result.length - searchResultLimit} others` + message: t('graphPanel.search.message', { count: result.length - searchResultLimit }) } ] }, @@ -118,7 +120,7 @@ export const GraphSearchInput = ({ if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null) }} label={'item'} - placeholder="Search nodes..." + placeholder={t('graphPanel.search.placeholder')} /> ) } diff --git a/lightrag_webui/src/components/graph/LayoutsControl.tsx b/lightrag_webui/src/components/graph/LayoutsControl.tsx index c57b371a..0ed97f2f 100644 --- a/lightrag_webui/src/components/graph/LayoutsControl.tsx +++ b/lightrag_webui/src/components/graph/LayoutsControl.tsx @@ -16,6 +16,7 @@ import { controlButtonVariant } from '@/lib/constants' import { useSettingsStore } from '@/stores/settings' import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' type LayoutName = | 'Circular' @@ -28,6 +29,7 @@ type LayoutName = const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => { const sigma = useSigma() const { stop, start, isRunning } = layout + const { t } = useTranslation() /** * Init component when Sigma or component settings change. @@ -61,7 +63,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) = @@ -166,7 +169,7 @@ const LayoutsControl = () => { key={name} className="cursor-pointer text-xs" > - {name} + {t(`graphPanel.sideBar.layoutsControl.layouts.${name}`)} ))} diff --git a/lightrag_webui/src/components/graph/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx index 325f9ff1..85afc343 100644 --- a/lightrag_webui/src/components/graph/PropertiesView.tsx +++ b/lightrag_webui/src/components/graph/PropertiesView.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph' import Text from '@/components/ui/Text' import useLightragGraph from '@/hooks/useLightragGraph' +import { useTranslation } from 'react-i18next' /** * Component that view properties of elements in graph. @@ -147,21 +148,22 @@ const PropertyRow = ({ } const NodePropertiesView = ({ node }: { node: NodeType }) => { + const { t } = useTranslation() return (
- +
- + { useGraphStore.getState().setSelectedNode(node.id, true) }} /> - +
- +
{Object.keys(node.properties) .sort() @@ -172,7 +174,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => { {node.relationships.length > 0 && ( <>
{node.relationships.map(({ type, id, label }) => { @@ -195,28 +197,29 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => { } const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => { + const { t } = useTranslation() return (
- +
- - {edge.type && } + + {edge.type && } { useGraphStore.getState().setSelectedNode(edge.source, true) }} /> { useGraphStore.getState().setSelectedNode(edge.target, true) }} />
- +
{Object.keys(edge.properties) .sort() diff --git a/lightrag_webui/src/components/graph/Settings.tsx b/lightrag_webui/src/components/graph/Settings.tsx index d9aa5523..8d764a9c 100644 --- a/lightrag_webui/src/components/graph/Settings.tsx +++ b/lightrag_webui/src/components/graph/Settings.tsx @@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings' import { useBackendState } from '@/stores/state' import { SettingsIcon, RefreshCwIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next'; /** * Component that displays a checkbox with a label. @@ -205,11 +206,13 @@ export default function Settings() { [setTempApiKey] ) + const { t } = useTranslation(); + return ( <> - @@ -231,7 +234,7 @@ export default function Settings() { @@ -239,12 +242,12 @@ export default function Settings() { @@ -252,12 +255,12 @@ export default function Settings() { @@ -265,51 +268,50 @@ export default function Settings() { -
- +
e.preventDefault()}>
@@ -320,7 +322,7 @@ export default function Settings() { size="sm" className="max-h-full shrink-0" > - Save + {t('graphPanel.sideBar.settings.save')}
diff --git a/lightrag_webui/src/components/graph/StatusCard.tsx b/lightrag_webui/src/components/graph/StatusCard.tsx index 3084d103..e67cbd30 100644 --- a/lightrag_webui/src/components/graph/StatusCard.tsx +++ b/lightrag_webui/src/components/graph/StatusCard.tsx @@ -1,58 +1,60 @@ import { LightragStatus } from '@/api/lightrag' +import { useTranslation } from 'react-i18next' const StatusCard = ({ status }: { status: LightragStatus | null }) => { + const { t } = useTranslation() if (!status) { - return
Status information unavailable
+ return
{t('graphPanel.statusCard.unavailable')}
} return (
-

Storage Info

+

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

- Working Directory: + {t('graphPanel.statusCard.workingDirectory')}: {status.working_directory} - Input Directory: + {t('graphPanel.statusCard.inputDirectory')}: {status.input_directory}
-

LLM Configuration

+

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

- LLM Binding: + {t('graphPanel.statusCard.llmBinding')}: {status.configuration.llm_binding} - LLM Binding Host: + {t('graphPanel.statusCard.llmBindingHost')}: {status.configuration.llm_binding_host} - LLM Model: + {t('graphPanel.statusCard.llmModel')}: {status.configuration.llm_model} - Max Tokens: + {t('graphPanel.statusCard.maxTokens')}: {status.configuration.max_tokens}
-

Embedding Configuration

+

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

- Embedding Binding: + {t('graphPanel.statusCard.embeddingBinding')}: {status.configuration.embedding_binding} - Embedding Binding Host: + {t('graphPanel.statusCard.embeddingBindingHost')}: {status.configuration.embedding_binding_host} - Embedding Model: + {t('graphPanel.statusCard.embeddingModel')}: {status.configuration.embedding_model}
-

Storage Configuration

+

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

- KV Storage: + {t('graphPanel.statusCard.kvStorage')}: {status.configuration.kv_storage} - Doc Status Storage: + {t('graphPanel.statusCard.docStatusStorage')}: {status.configuration.doc_status_storage} - Graph Storage: + {t('graphPanel.statusCard.graphStorage')}: {status.configuration.graph_storage} - Vector Storage: + {t('graphPanel.statusCard.vectorStorage')}: {status.configuration.vector_storage}
diff --git a/lightrag_webui/src/components/graph/StatusIndicator.tsx b/lightrag_webui/src/components/graph/StatusIndicator.tsx index 3272d9fa..d7a1831f 100644 --- a/lightrag_webui/src/components/graph/StatusIndicator.tsx +++ b/lightrag_webui/src/components/graph/StatusIndicator.tsx @@ -3,8 +3,10 @@ import { useBackendState } from '@/stores/state' import { useEffect, useState } from 'react' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' import StatusCard from '@/components/graph/StatusCard' +import { useTranslation } from 'react-i18next' const StatusIndicator = () => { + const { t } = useTranslation() const health = useBackendState.use.health() const lastCheckTime = useBackendState.use.lastCheckTime() const status = useBackendState.use.status() @@ -33,7 +35,7 @@ const StatusIndicator = () => { )} /> - {health ? 'Connected' : 'Disconnected'} + {health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')}
diff --git a/lightrag_webui/src/components/graph/ZoomControl.tsx b/lightrag_webui/src/components/graph/ZoomControl.tsx index 790b4423..0aa55416 100644 --- a/lightrag_webui/src/components/graph/ZoomControl.tsx +++ b/lightrag_webui/src/components/graph/ZoomControl.tsx @@ -3,12 +3,14 @@ import { useCallback } from 'react' import Button from '@/components/ui/Button' import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react' import { controlButtonVariant } from '@/lib/constants' +import { useTranslation } from "react-i18next"; /** * Component that provides zoom controls for the graph viewer. */ const ZoomControl = () => { const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 }) + const { t } = useTranslation(); const handleZoomIn = useCallback(() => zoomIn(), [zoomIn]) const handleZoomOut = useCallback(() => zoomOut(), [zoomOut]) @@ -16,16 +18,16 @@ const ZoomControl = () => { return ( <> - -
@@ -98,29 +100,29 @@ export default function DocumentManager() { - Uploaded documents - view the uploaded documents here + {t('documentPanel.documentManager.uploadedTitle')} + {t('documentPanel.documentManager.uploadedDescription')} {!docs && ( )} {docs && ( - ID - Summary - Status - Length - Chunks - Created - Updated - Metadata + {t('documentPanel.documentManager.columns.id')} + {t('documentPanel.documentManager.columns.summary')} + {t('documentPanel.documentManager.columns.status')} + {t('documentPanel.documentManager.columns.length')} + {t('documentPanel.documentManager.columns.chunks')} + {t('documentPanel.documentManager.columns.created')} + {t('documentPanel.documentManager.columns.updated')} + {t('documentPanel.documentManager.columns.metadata')} @@ -137,13 +139,13 @@ export default function DocumentManager() { {status === 'processed' && ( - Completed + {t('documentPanel.documentManager.status.completed')} )} {status === 'processing' && ( - Processing + {t('documentPanel.documentManager.status.processing')} )} - {status === 'pending' && Pending} - {status === 'failed' && Failed} + {status === 'pending' && {t('documentPanel.documentManager.status.pending')}} + {status === 'failed' && {t('documentPanel.documentManager.status.failed')}} {doc.error && ( ⚠️ diff --git a/lightrag_webui/src/features/RetrievalTesting.tsx b/lightrag_webui/src/features/RetrievalTesting.tsx index 340255a2..c7fdf2a9 100644 --- a/lightrag_webui/src/features/RetrievalTesting.tsx +++ b/lightrag_webui/src/features/RetrievalTesting.tsx @@ -8,8 +8,10 @@ import { useDebounce } from '@/hooks/useDebounce' import QuerySettings from '@/components/retrieval/QuerySettings' import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage' import { EraserIcon, SendIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' export default function RetrievalTesting() { + const { t } = useTranslation() const [messages, setMessages] = useState( () => useSettingsStore.getState().retrievalHistory || [] ) @@ -89,7 +91,7 @@ export default function RetrievalTesting() { } } catch (err) { // Handle error - updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`, true) + updateAssistantMessage(`${t('retrievePanel.retrieval.error')}\n${errorMessage(err)}`, true) } finally { // Clear loading and add messages to state setIsLoading(false) @@ -98,7 +100,7 @@ export default function RetrievalTesting() { .setRetrievalHistory([...prevMessages, userMessage, assistantMessage]) } }, - [inputValue, isLoading, messages, setMessages] + [inputValue, isLoading, messages, setMessages, t] ) const debouncedMessages = useDebounce(messages, 100) @@ -117,7 +119,7 @@ export default function RetrievalTesting() {
{messages.length === 0 ? (
- Start a retrieval by typing your query below + {t('retrievePanel.retrieval.startPrompt')}
) : ( messages.map((message, idx) => ( @@ -143,18 +145,18 @@ export default function RetrievalTesting() { size="sm" > - Clear + {t('retrievePanel.retrieval.clear')} setInputValue(e.target.value)} - placeholder="Type your query..." + placeholder={t('retrievePanel.retrieval.placeholder')} disabled={isLoading} />
diff --git a/lightrag_webui/src/features/SiteHeader.tsx b/lightrag_webui/src/features/SiteHeader.tsx index c09ce089..ac3bdd70 100644 --- a/lightrag_webui/src/features/SiteHeader.tsx +++ b/lightrag_webui/src/features/SiteHeader.tsx @@ -4,6 +4,7 @@ import ThemeToggle from '@/components/ThemeToggle' import { TabsList, TabsTrigger } from '@/components/ui/Tabs' import { useSettingsStore } from '@/stores/settings' import { cn } from '@/lib/utils' +import { useTranslation } from 'react-i18next' import { ZapIcon, GithubIcon } from 'lucide-react' @@ -29,21 +30,22 @@ function NavigationTab({ value, currentTab, children }: NavigationTabProps) { function TabsNavigation() { const currentTab = useSettingsStore.use.currentTab() + const { t } = useTranslation() return (
- Documents + {t('header.documents')} - Knowledge Graph + {t('header.knowledgeGraph')} - Retrieval + {t('header.retrieval')} - API + {t('header.api')}
@@ -51,6 +53,7 @@ function TabsNavigation() { } export default function SiteHeader() { + const { t } = useTranslation() return (
@@ -64,7 +67,7 @@ export default function SiteHeader() {