yangdx commited on
Commit
2000c22
·
2 Parent(s): d57642a 822a4ff

Merge branch 'main' into improve-property-tooltip

Browse files
Files changed (34) hide show
  1. lightrag/base.py +24 -0
  2. lightrag/kg/chroma_impl.py +67 -1
  3. lightrag/kg/faiss_impl.py +46 -1
  4. lightrag/kg/milvus_impl.py +57 -1
  5. lightrag/kg/mongo_impl.py +56 -1
  6. lightrag/kg/nano_vector_db_impl.py +33 -1
  7. lightrag/kg/oracle_impl.py +77 -1
  8. lightrag/kg/postgres_impl.py +54 -0
  9. lightrag/kg/qdrant_impl.py +3 -1
  10. lightrag/kg/tidb_impl.py +97 -1
  11. lightrag/lightrag.py +6 -32
  12. lightrag_webui/bun.lock +13 -0
  13. lightrag_webui/package.json +3 -0
  14. lightrag_webui/src/components/ThemeToggle.tsx +4 -2
  15. lightrag_webui/src/components/documents/ClearDocumentsDialog.tsx +10 -8
  16. lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx +12 -10
  17. lightrag_webui/src/components/graph/FullScreenControl.tsx +4 -2
  18. lightrag_webui/src/components/graph/GraphLabels.tsx +6 -4
  19. lightrag_webui/src/components/graph/GraphSearch.tsx +4 -2
  20. lightrag_webui/src/components/graph/LayoutsControl.tsx +6 -3
  21. lightrag_webui/src/components/graph/PropertiesView.tsx +15 -12
  22. lightrag_webui/src/components/graph/Settings.tsx +19 -17
  23. lightrag_webui/src/components/graph/StatusCard.tsx +20 -18
  24. lightrag_webui/src/components/graph/StatusIndicator.tsx +3 -1
  25. lightrag_webui/src/components/graph/ZoomControl.tsx +5 -3
  26. lightrag_webui/src/components/retrieval/ChatMessage.tsx +5 -2
  27. lightrag_webui/src/components/retrieval/QuerySettings.tsx +43 -41
  28. lightrag_webui/src/features/DocumentManager.tsx +24 -22
  29. lightrag_webui/src/features/RetrievalTesting.tsx +8 -6
  30. lightrag_webui/src/features/SiteHeader.tsx +8 -5
  31. lightrag_webui/src/i18n.js +21 -0
  32. lightrag_webui/src/locales/en.json +235 -0
  33. lightrag_webui/src/locales/zh.json +236 -0
  34. lightrag_webui/src/main.tsx +2 -0
lightrag/base.py CHANGED
@@ -127,6 +127,30 @@ class BaseVectorStorage(StorageNameSpace, ABC):
127
  async def delete_entity_relation(self, entity_name: str) -> None:
128
  """Delete relations for a given entity."""
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  @dataclass
132
  class BaseKVStorage(StorageNameSpace, ABC):
 
127
  async def delete_entity_relation(self, entity_name: str) -> None:
128
  """Delete relations for a given entity."""
129
 
130
+ @abstractmethod
131
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
132
+ """Get vector data by its ID
133
+
134
+ Args:
135
+ id: The unique identifier of the vector
136
+
137
+ Returns:
138
+ The vector data if found, or None if not found
139
+ """
140
+ pass
141
+
142
+ @abstractmethod
143
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
144
+ """Get multiple vector data by their IDs
145
+
146
+ Args:
147
+ ids: List of unique identifiers
148
+
149
+ Returns:
150
+ List of vector data objects that were found
151
+ """
152
+ pass
153
+
154
 
155
  @dataclass
156
  class BaseKVStorage(StorageNameSpace, ABC):
lightrag/kg/chroma_impl.py CHANGED
@@ -156,7 +156,9 @@ class ChromaVectorDBStorage(BaseVectorStorage):
156
  logger.error(f"Error during ChromaDB upsert: {str(e)}")
157
  raise
158
 
159
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
160
  try:
161
  embedding = await self.embedding_func([query])
162
 
@@ -269,3 +271,67 @@ class ChromaVectorDBStorage(BaseVectorStorage):
269
  except Exception as e:
270
  logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
271
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  logger.error(f"Error during ChromaDB upsert: {str(e)}")
157
  raise
158
 
159
+ async def query(
160
+ self, query: str, top_k: int, ids: list[str] | None = None
161
+ ) -> list[dict[str, Any]]:
162
  try:
163
  embedding = await self.embedding_func([query])
164
 
 
271
  except Exception as e:
272
  logger.error(f"Error during prefix search in ChromaDB: {str(e)}")
273
  raise
274
+
275
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
276
+ """Get vector data by its ID
277
+
278
+ Args:
279
+ id: The unique identifier of the vector
280
+
281
+ Returns:
282
+ The vector data if found, or None if not found
283
+ """
284
+ try:
285
+ # Query the collection for a single vector by ID
286
+ result = self._collection.get(
287
+ ids=[id], include=["metadatas", "embeddings", "documents"]
288
+ )
289
+
290
+ if not result or not result["ids"] or len(result["ids"]) == 0:
291
+ return None
292
+
293
+ # Format the result to match the expected structure
294
+ return {
295
+ "id": result["ids"][0],
296
+ "vector": result["embeddings"][0],
297
+ "content": result["documents"][0],
298
+ **result["metadatas"][0],
299
+ }
300
+ except Exception as e:
301
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
302
+ return None
303
+
304
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
305
+ """Get multiple vector data by their IDs
306
+
307
+ Args:
308
+ ids: List of unique identifiers
309
+
310
+ Returns:
311
+ List of vector data objects that were found
312
+ """
313
+ if not ids:
314
+ return []
315
+
316
+ try:
317
+ # Query the collection for multiple vectors by IDs
318
+ result = self._collection.get(
319
+ ids=ids, include=["metadatas", "embeddings", "documents"]
320
+ )
321
+
322
+ if not result or not result["ids"] or len(result["ids"]) == 0:
323
+ return []
324
+
325
+ # Format the results to match the expected structure
326
+ return [
327
+ {
328
+ "id": result["ids"][i],
329
+ "vector": result["embeddings"][i],
330
+ "content": result["documents"][i],
331
+ **result["metadatas"][i],
332
+ }
333
+ for i in range(len(result["ids"]))
334
+ ]
335
+ except Exception as e:
336
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
337
+ return []
lightrag/kg/faiss_impl.py CHANGED
@@ -171,7 +171,9 @@ class FaissVectorDBStorage(BaseVectorStorage):
171
  logger.info(f"Upserted {len(list_data)} vectors into Faiss index.")
172
  return [m["__id__"] for m in list_data]
173
 
174
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
175
  """
176
  Search by a textual query; returns top_k results with their metadata + similarity distance.
177
  """
@@ -392,3 +394,46 @@ class FaissVectorDBStorage(BaseVectorStorage):
392
 
393
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
394
  return matching_records
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  logger.info(f"Upserted {len(list_data)} vectors into Faiss index.")
172
  return [m["__id__"] for m in list_data]
173
 
174
+ async def query(
175
+ self, query: str, top_k: int, ids: list[str] | None = None
176
+ ) -> list[dict[str, Any]]:
177
  """
178
  Search by a textual query; returns top_k results with their metadata + similarity distance.
179
  """
 
394
 
395
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
396
  return matching_records
397
+
398
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
399
+ """Get vector data by its ID
400
+
401
+ Args:
402
+ id: The unique identifier of the vector
403
+
404
+ Returns:
405
+ The vector data if found, or None if not found
406
+ """
407
+ # Find the Faiss internal ID for the custom ID
408
+ fid = self._find_faiss_id_by_custom_id(id)
409
+ if fid is None:
410
+ return None
411
+
412
+ # Get the metadata for the found ID
413
+ metadata = self._id_to_meta.get(fid, {})
414
+ if not metadata:
415
+ return None
416
+
417
+ return {**metadata, "id": metadata.get("__id__")}
418
+
419
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
420
+ """Get multiple vector data by their IDs
421
+
422
+ Args:
423
+ ids: List of unique identifiers
424
+
425
+ Returns:
426
+ List of vector data objects that were found
427
+ """
428
+ if not ids:
429
+ return []
430
+
431
+ results = []
432
+ for id in ids:
433
+ fid = self._find_faiss_id_by_custom_id(id)
434
+ if fid is not None:
435
+ metadata = self._id_to_meta.get(fid, {})
436
+ if metadata:
437
+ results.append({**metadata, "id": metadata.get("__id__")})
438
+
439
+ return results
lightrag/kg/milvus_impl.py CHANGED
@@ -101,7 +101,9 @@ class MilvusVectorDBStorage(BaseVectorStorage):
101
  results = self._client.upsert(collection_name=self.namespace, data=list_data)
102
  return results
103
 
104
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
105
  embedding = await self.embedding_func([query])
106
  results = self._client.search(
107
  collection_name=self.namespace,
@@ -231,3 +233,57 @@ class MilvusVectorDBStorage(BaseVectorStorage):
231
  except Exception as e:
232
  logger.error(f"Error searching for records with prefix '{prefix}': {e}")
233
  return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  results = self._client.upsert(collection_name=self.namespace, data=list_data)
102
  return results
103
 
104
+ async def query(
105
+ self, query: str, top_k: int, ids: list[str] | None = None
106
+ ) -> list[dict[str, Any]]:
107
  embedding = await self.embedding_func([query])
108
  results = self._client.search(
109
  collection_name=self.namespace,
 
233
  except Exception as e:
234
  logger.error(f"Error searching for records with prefix '{prefix}': {e}")
235
  return []
236
+
237
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
238
+ """Get vector data by its ID
239
+
240
+ Args:
241
+ id: The unique identifier of the vector
242
+
243
+ Returns:
244
+ The vector data if found, or None if not found
245
+ """
246
+ try:
247
+ # Query Milvus for a specific ID
248
+ result = self._client.query(
249
+ collection_name=self.namespace,
250
+ filter=f'id == "{id}"',
251
+ output_fields=list(self.meta_fields) + ["id"],
252
+ )
253
+
254
+ if not result or len(result) == 0:
255
+ return None
256
+
257
+ return result[0]
258
+ except Exception as e:
259
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
260
+ return None
261
+
262
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
263
+ """Get multiple vector data by their IDs
264
+
265
+ Args:
266
+ ids: List of unique identifiers
267
+
268
+ Returns:
269
+ List of vector data objects that were found
270
+ """
271
+ if not ids:
272
+ return []
273
+
274
+ try:
275
+ # Prepare the ID filter expression
276
+ id_list = '", "'.join(ids)
277
+ filter_expr = f'id in ["{id_list}"]'
278
+
279
+ # Query Milvus with the filter
280
+ result = self._client.query(
281
+ collection_name=self.namespace,
282
+ filter=filter_expr,
283
+ output_fields=list(self.meta_fields) + ["id"],
284
+ )
285
+
286
+ return result or []
287
+ except Exception as e:
288
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
289
+ return []
lightrag/kg/mongo_impl.py CHANGED
@@ -938,7 +938,9 @@ class MongoVectorDBStorage(BaseVectorStorage):
938
 
939
  return list_data
940
 
941
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
942
  """Queries the vector database using Atlas Vector Search."""
943
  # Generate the embedding
944
  embedding = await self.embedding_func([query])
@@ -1071,6 +1073,59 @@ class MongoVectorDBStorage(BaseVectorStorage):
1071
  logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
1072
  return []
1073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
 
1075
  async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
1076
  collection_names = await db.list_collection_names()
 
938
 
939
  return list_data
940
 
941
+ async def query(
942
+ self, query: str, top_k: int, ids: list[str] | None = None
943
+ ) -> list[dict[str, Any]]:
944
  """Queries the vector database using Atlas Vector Search."""
945
  # Generate the embedding
946
  embedding = await self.embedding_func([query])
 
1073
  logger.error(f"Error searching by prefix in {self.namespace}: {str(e)}")
1074
  return []
1075
 
1076
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
1077
+ """Get vector data by its ID
1078
+
1079
+ Args:
1080
+ id: The unique identifier of the vector
1081
+
1082
+ Returns:
1083
+ The vector data if found, or None if not found
1084
+ """
1085
+ try:
1086
+ # Search for the specific ID in MongoDB
1087
+ result = await self._data.find_one({"_id": id})
1088
+ if result:
1089
+ # Format the result to include id field expected by API
1090
+ result_dict = dict(result)
1091
+ if "_id" in result_dict and "id" not in result_dict:
1092
+ result_dict["id"] = result_dict["_id"]
1093
+ return result_dict
1094
+ return None
1095
+ except Exception as e:
1096
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
1097
+ return None
1098
+
1099
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
1100
+ """Get multiple vector data by their IDs
1101
+
1102
+ Args:
1103
+ ids: List of unique identifiers
1104
+
1105
+ Returns:
1106
+ List of vector data objects that were found
1107
+ """
1108
+ if not ids:
1109
+ return []
1110
+
1111
+ try:
1112
+ # Query MongoDB for multiple IDs
1113
+ cursor = self._data.find({"_id": {"$in": ids}})
1114
+ results = await cursor.to_list(length=None)
1115
+
1116
+ # Format results to include id field expected by API
1117
+ formatted_results = []
1118
+ for result in results:
1119
+ result_dict = dict(result)
1120
+ if "_id" in result_dict and "id" not in result_dict:
1121
+ result_dict["id"] = result_dict["_id"]
1122
+ formatted_results.append(result_dict)
1123
+
1124
+ return formatted_results
1125
+ except Exception as e:
1126
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
1127
+ return []
1128
+
1129
 
1130
  async def get_or_create_collection(db: AsyncIOMotorDatabase, collection_name: str):
1131
  collection_names = await db.list_collection_names()
lightrag/kg/nano_vector_db_impl.py CHANGED
@@ -120,7 +120,9 @@ class NanoVectorDBStorage(BaseVectorStorage):
120
  f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}"
121
  )
122
 
123
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
124
  # Execute embedding outside of lock to avoid long lock times
125
  embedding = await self.embedding_func([query])
126
  embedding = embedding[0]
@@ -256,3 +258,33 @@ class NanoVectorDBStorage(BaseVectorStorage):
256
 
257
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
258
  return matching_records
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  f"embedding is not 1-1 with data, {len(embeddings)} != {len(list_data)}"
121
  )
122
 
123
+ async def query(
124
+ self, query: str, top_k: int, ids: list[str] | None = None
125
+ ) -> list[dict[str, Any]]:
126
  # Execute embedding outside of lock to avoid long lock times
127
  embedding = await self.embedding_func([query])
128
  embedding = embedding[0]
 
258
 
259
  logger.debug(f"Found {len(matching_records)} records with prefix '{prefix}'")
260
  return matching_records
261
+
262
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
263
+ """Get vector data by its ID
264
+
265
+ Args:
266
+ id: The unique identifier of the vector
267
+
268
+ Returns:
269
+ The vector data if found, or None if not found
270
+ """
271
+ client = await self._get_client()
272
+ result = client.get([id])
273
+ if result:
274
+ return result[0]
275
+ return None
276
+
277
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
278
+ """Get multiple vector data by their IDs
279
+
280
+ Args:
281
+ ids: List of unique identifiers
282
+
283
+ Returns:
284
+ List of vector data objects that were found
285
+ """
286
+ if not ids:
287
+ return []
288
+
289
+ client = await self._get_client()
290
+ return client.get(ids)
lightrag/kg/oracle_impl.py CHANGED
@@ -417,7 +417,9 @@ class OracleVectorDBStorage(BaseVectorStorage):
417
  self.db = None
418
 
419
  #################### query method ###############
420
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
421
  embeddings = await self.embedding_func([query])
422
  embedding = embeddings[0]
423
  # 转换精度
@@ -529,6 +531,80 @@ class OracleVectorDBStorage(BaseVectorStorage):
529
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
530
  return []
531
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
  @final
534
  @dataclass
 
417
  self.db = None
418
 
419
  #################### query method ###############
420
+ async def query(
421
+ self, query: str, top_k: int, ids: list[str] | None = None
422
+ ) -> list[dict[str, Any]]:
423
  embeddings = await self.embedding_func([query])
424
  embedding = embeddings[0]
425
  # 转换精度
 
531
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
532
  return []
533
 
534
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
535
+ """Get vector data by its ID
536
+
537
+ Args:
538
+ id: The unique identifier of the vector
539
+
540
+ Returns:
541
+ The vector data if found, or None if not found
542
+ """
543
+ try:
544
+ # Determine the table name based on namespace
545
+ table_name = namespace_to_table_name(self.namespace)
546
+ if not table_name:
547
+ logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
548
+ return None
549
+
550
+ # Create the appropriate ID field name based on namespace
551
+ id_field = "entity_id" if "NODES" in table_name else "relation_id"
552
+ if "CHUNKS" in table_name:
553
+ id_field = "chunk_id"
554
+
555
+ # Prepare and execute the query
556
+ query = f"""
557
+ SELECT * FROM {table_name}
558
+ WHERE {id_field} = :id AND workspace = :workspace
559
+ """
560
+ params = {"id": id, "workspace": self.db.workspace}
561
+
562
+ result = await self.db.query(query, params)
563
+ return result
564
+ except Exception as e:
565
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
566
+ return None
567
+
568
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
569
+ """Get multiple vector data by their IDs
570
+
571
+ Args:
572
+ ids: List of unique identifiers
573
+
574
+ Returns:
575
+ List of vector data objects that were found
576
+ """
577
+ if not ids:
578
+ return []
579
+
580
+ try:
581
+ # Determine the table name based on namespace
582
+ table_name = namespace_to_table_name(self.namespace)
583
+ if not table_name:
584
+ logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
585
+ return []
586
+
587
+ # Create the appropriate ID field name based on namespace
588
+ id_field = "entity_id" if "NODES" in table_name else "relation_id"
589
+ if "CHUNKS" in table_name:
590
+ id_field = "chunk_id"
591
+
592
+ # Format the list of IDs for SQL IN clause
593
+ ids_list = ", ".join([f"'{id}'" for id in ids])
594
+
595
+ # Prepare and execute the query
596
+ query = f"""
597
+ SELECT * FROM {table_name}
598
+ WHERE {id_field} IN ({ids_list}) AND workspace = :workspace
599
+ """
600
+ params = {"workspace": self.db.workspace}
601
+
602
+ results = await self.db.query(query, params, multirows=True)
603
+ return results or []
604
+ except Exception as e:
605
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
606
+ return []
607
+
608
 
609
  @final
610
  @dataclass
lightrag/kg/postgres_impl.py CHANGED
@@ -621,6 +621,60 @@ class PGVectorStorage(BaseVectorStorage):
621
  logger.error(f"Error during prefix search for '{prefix}': {e}")
622
  return []
623
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
 
625
  @final
626
  @dataclass
 
621
  logger.error(f"Error during prefix search for '{prefix}': {e}")
622
  return []
623
 
624
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
625
+ """Get vector data by its ID
626
+
627
+ Args:
628
+ id: The unique identifier of the vector
629
+
630
+ Returns:
631
+ The vector data if found, or None if not found
632
+ """
633
+ table_name = namespace_to_table_name(self.namespace)
634
+ if not table_name:
635
+ logger.error(f"Unknown namespace for ID lookup: {self.namespace}")
636
+ return None
637
+
638
+ query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id=$2"
639
+ params = {"workspace": self.db.workspace, "id": id}
640
+
641
+ try:
642
+ result = await self.db.query(query, params)
643
+ if result:
644
+ return dict(result)
645
+ return None
646
+ except Exception as e:
647
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
648
+ return None
649
+
650
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
651
+ """Get multiple vector data by their IDs
652
+
653
+ Args:
654
+ ids: List of unique identifiers
655
+
656
+ Returns:
657
+ List of vector data objects that were found
658
+ """
659
+ if not ids:
660
+ return []
661
+
662
+ table_name = namespace_to_table_name(self.namespace)
663
+ if not table_name:
664
+ logger.error(f"Unknown namespace for IDs lookup: {self.namespace}")
665
+ return []
666
+
667
+ ids_str = ",".join([f"'{id}'" for id in ids])
668
+ query = f"SELECT * FROM {table_name} WHERE workspace=$1 AND id IN ({ids_str})"
669
+ params = {"workspace": self.db.workspace}
670
+
671
+ try:
672
+ results = await self.db.query(query, params, multirows=True)
673
+ return [dict(record) for record in results]
674
+ except Exception as e:
675
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
676
+ return []
677
+
678
 
679
  @final
680
  @dataclass
lightrag/kg/qdrant_impl.py CHANGED
@@ -123,7 +123,9 @@ class QdrantVectorDBStorage(BaseVectorStorage):
123
  )
124
  return results
125
 
126
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
127
  embedding = await self.embedding_func([query])
128
  results = self._client.search(
129
  collection_name=self.namespace,
 
123
  )
124
  return results
125
 
126
+ async def query(
127
+ self, query: str, top_k: int, ids: list[str] | None = None
128
+ ) -> list[dict[str, Any]]:
129
  embedding = await self.embedding_func([query])
130
  results = self._client.search(
131
  collection_name=self.namespace,
lightrag/kg/tidb_impl.py CHANGED
@@ -306,7 +306,9 @@ class TiDBVectorDBStorage(BaseVectorStorage):
306
  await ClientManager.release_client(self.db)
307
  self.db = None
308
 
309
- async def query(self, query: str, top_k: int) -> list[dict[str, Any]]:
 
 
310
  """Search from tidb vector"""
311
  embeddings = await self.embedding_func([query])
312
  embedding = embeddings[0]
@@ -463,6 +465,100 @@ class TiDBVectorDBStorage(BaseVectorStorage):
463
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
464
  return []
465
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  @final
468
  @dataclass
 
306
  await ClientManager.release_client(self.db)
307
  self.db = None
308
 
309
+ async def query(
310
+ self, query: str, top_k: int, ids: list[str] | None = None
311
+ ) -> list[dict[str, Any]]:
312
  """Search from tidb vector"""
313
  embeddings = await self.embedding_func([query])
314
  embedding = embeddings[0]
 
465
  logger.error(f"Error searching records with prefix '{prefix}': {e}")
466
  return []
467
 
468
+ async def get_by_id(self, id: str) -> dict[str, Any] | None:
469
+ """Get vector data by its ID
470
+
471
+ Args:
472
+ id: The unique identifier of the vector
473
+
474
+ Returns:
475
+ The vector data if found, or None if not found
476
+ """
477
+ try:
478
+ # Determine which table to query based on namespace
479
+ if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
480
+ sql_template = """
481
+ SELECT entity_id as id, name as entity_name, entity_type, description, content
482
+ FROM LIGHTRAG_GRAPH_NODES
483
+ WHERE entity_id = :entity_id AND workspace = :workspace
484
+ """
485
+ params = {"entity_id": id, "workspace": self.db.workspace}
486
+ elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
487
+ sql_template = """
488
+ SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
489
+ keywords, description, content
490
+ FROM LIGHTRAG_GRAPH_EDGES
491
+ WHERE relation_id = :relation_id AND workspace = :workspace
492
+ """
493
+ params = {"relation_id": id, "workspace": self.db.workspace}
494
+ elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
495
+ sql_template = """
496
+ SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
497
+ FROM LIGHTRAG_DOC_CHUNKS
498
+ WHERE chunk_id = :chunk_id AND workspace = :workspace
499
+ """
500
+ params = {"chunk_id": id, "workspace": self.db.workspace}
501
+ else:
502
+ logger.warning(
503
+ f"Namespace {self.namespace} not supported for get_by_id"
504
+ )
505
+ return None
506
+
507
+ result = await self.db.query(sql_template, params=params)
508
+ return result
509
+ except Exception as e:
510
+ logger.error(f"Error retrieving vector data for ID {id}: {e}")
511
+ return None
512
+
513
+ async def get_by_ids(self, ids: list[str]) -> list[dict[str, Any]]:
514
+ """Get multiple vector data by their IDs
515
+
516
+ Args:
517
+ ids: List of unique identifiers
518
+
519
+ Returns:
520
+ List of vector data objects that were found
521
+ """
522
+ if not ids:
523
+ return []
524
+
525
+ try:
526
+ # Format IDs for SQL IN clause
527
+ ids_str = ", ".join([f"'{id}'" for id in ids])
528
+
529
+ # Determine which table to query based on namespace
530
+ if self.namespace == NameSpace.VECTOR_STORE_ENTITIES:
531
+ sql_template = f"""
532
+ SELECT entity_id as id, name as entity_name, entity_type, description, content
533
+ FROM LIGHTRAG_GRAPH_NODES
534
+ WHERE entity_id IN ({ids_str}) AND workspace = :workspace
535
+ """
536
+ elif self.namespace == NameSpace.VECTOR_STORE_RELATIONSHIPS:
537
+ sql_template = f"""
538
+ SELECT relation_id as id, source_name as src_id, target_name as tgt_id,
539
+ keywords, description, content
540
+ FROM LIGHTRAG_GRAPH_EDGES
541
+ WHERE relation_id IN ({ids_str}) AND workspace = :workspace
542
+ """
543
+ elif self.namespace == NameSpace.VECTOR_STORE_CHUNKS:
544
+ sql_template = f"""
545
+ SELECT chunk_id as id, content, tokens, chunk_order_index, full_doc_id
546
+ FROM LIGHTRAG_DOC_CHUNKS
547
+ WHERE chunk_id IN ({ids_str}) AND workspace = :workspace
548
+ """
549
+ else:
550
+ logger.warning(
551
+ f"Namespace {self.namespace} not supported for get_by_ids"
552
+ )
553
+ return []
554
+
555
+ params = {"workspace": self.db.workspace}
556
+ results = await self.db.query(sql_template, params=params, multirows=True)
557
+ return results if results else []
558
+ except Exception as e:
559
+ logger.error(f"Error retrieving vector data for IDs {ids}: {e}")
560
+ return []
561
+
562
 
563
  @final
564
  @dataclass
lightrag/lightrag.py CHANGED
@@ -1710,19 +1710,7 @@ class LightRAG:
1710
  async def get_entity_info(
1711
  self, entity_name: str, include_vector_data: bool = False
1712
  ) -> dict[str, str | None | dict[str, str]]:
1713
- """Get detailed information of an entity
1714
-
1715
- Args:
1716
- entity_name: Entity name (no need for quotes)
1717
- include_vector_data: Whether to include data from the vector database
1718
-
1719
- Returns:
1720
- dict: A dictionary containing entity information, including:
1721
- - entity_name: Entity name
1722
- - source_id: Source document ID
1723
- - graph_data: Complete node data from the graph database
1724
- - vector_data: (optional) Data from the vector database
1725
- """
1726
 
1727
  # Get information from the graph
1728
  node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
@@ -1737,29 +1725,15 @@ class LightRAG:
1737
  # Optional: Get vector database information
1738
  if include_vector_data:
1739
  entity_id = compute_mdhash_id(entity_name, prefix="ent-")
1740
- vector_data = self.entities_vdb._client.get([entity_id])
1741
- result["vector_data"] = vector_data[0] if vector_data else None
1742
 
1743
  return result
1744
 
1745
  async def get_relation_info(
1746
  self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
1747
  ) -> dict[str, str | None | dict[str, str]]:
1748
- """Get detailed information of a relationship
1749
-
1750
- Args:
1751
- src_entity: Source entity name (no need for quotes)
1752
- tgt_entity: Target entity name (no need for quotes)
1753
- include_vector_data: Whether to include data from the vector database
1754
-
1755
- Returns:
1756
- dict: A dictionary containing relationship information, including:
1757
- - src_entity: Source entity name
1758
- - tgt_entity: Target entity name
1759
- - source_id: Source document ID
1760
- - graph_data: Complete edge data from the graph database
1761
- - vector_data: (optional) Data from the vector database
1762
- """
1763
 
1764
  # Get information from the graph
1765
  edge_data = await self.chunk_entity_relation_graph.get_edge(
@@ -1777,8 +1751,8 @@ class LightRAG:
1777
  # Optional: Get vector database information
1778
  if include_vector_data:
1779
  rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
1780
- vector_data = self.relationships_vdb._client.get([rel_id])
1781
- result["vector_data"] = vector_data[0] if vector_data else None
1782
 
1783
  return result
1784
 
 
1710
  async def get_entity_info(
1711
  self, entity_name: str, include_vector_data: bool = False
1712
  ) -> dict[str, str | None | dict[str, str]]:
1713
+ """Get detailed information of an entity"""
 
 
 
 
 
 
 
 
 
 
 
 
1714
 
1715
  # Get information from the graph
1716
  node_data = await self.chunk_entity_relation_graph.get_node(entity_name)
 
1725
  # Optional: Get vector database information
1726
  if include_vector_data:
1727
  entity_id = compute_mdhash_id(entity_name, prefix="ent-")
1728
+ vector_data = await self.entities_vdb.get_by_id(entity_id)
1729
+ result["vector_data"] = vector_data
1730
 
1731
  return result
1732
 
1733
  async def get_relation_info(
1734
  self, src_entity: str, tgt_entity: str, include_vector_data: bool = False
1735
  ) -> dict[str, str | None | dict[str, str]]:
1736
+ """Get detailed information of a relationship"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1737
 
1738
  # Get information from the graph
1739
  edge_data = await self.chunk_entity_relation_graph.get_edge(
 
1751
  # Optional: Get vector database information
1752
  if include_vector_data:
1753
  rel_id = compute_mdhash_id(src_entity + tgt_entity, prefix="rel-")
1754
+ vector_data = await self.relationships_vdb.get_by_id(rel_id)
1755
+ result["vector_data"] = vector_data
1756
 
1757
  return result
1758
 
lightrag_webui/bun.lock CHANGED
@@ -34,11 +34,13 @@
34
  "cmdk": "^1.0.4",
35
  "graphology": "^0.26.0",
36
  "graphology-generators": "^0.11.2",
 
37
  "lucide-react": "^0.475.0",
38
  "minisearch": "^7.1.2",
39
  "react": "^19.0.0",
40
  "react-dom": "^19.0.0",
41
  "react-dropzone": "^14.3.6",
 
42
  "react-markdown": "^9.1.0",
43
  "react-number-format": "^5.4.3",
44
  "react-syntax-highlighter": "^15.6.1",
@@ -60,6 +62,7 @@
60
  "@types/node": "^22.13.5",
61
  "@types/react": "^19.0.10",
62
  "@types/react-dom": "^19.0.4",
 
63
  "@types/react-syntax-highlighter": "^15.5.13",
64
  "@types/seedrandom": "^3.0.8",
65
  "@vitejs/plugin-react-swc": "^3.8.0",
@@ -441,6 +444,8 @@
441
 
442
  "@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
443
 
 
 
444
  "@types/react-syntax-highlighter": ["@types/[email protected]", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
445
 
446
  "@types/react-transition-group": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
@@ -765,8 +770,12 @@
765
 
766
  "hoist-non-react-statics": ["[email protected]", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
767
 
 
 
768
  "html-url-attributes": ["[email protected]", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
769
 
 
 
770
  "ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
771
 
772
  "import-fresh": ["[email protected]", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -1093,6 +1102,8 @@
1093
 
1094
  "react-dropzone": ["[email protected]", "", { "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=="],
1095
 
 
 
1096
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
1097
 
1098
  "react-markdown": ["[email protected]", "", { "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 @@
1271
 
1272
  "vite": ["[email protected]", "", { "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=="],
1273
 
 
 
1274
  "which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
1275
 
1276
  "which-boxed-primitive": ["[email protected]", "", { "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=="],
 
34
  "cmdk": "^1.0.4",
35
  "graphology": "^0.26.0",
36
  "graphology-generators": "^0.11.2",
37
+ "i18next": "^24.2.2",
38
  "lucide-react": "^0.475.0",
39
  "minisearch": "^7.1.2",
40
  "react": "^19.0.0",
41
  "react-dom": "^19.0.0",
42
  "react-dropzone": "^14.3.6",
43
+ "react-i18next": "^15.4.1",
44
  "react-markdown": "^9.1.0",
45
  "react-number-format": "^5.4.3",
46
  "react-syntax-highlighter": "^15.6.1",
 
62
  "@types/node": "^22.13.5",
63
  "@types/react": "^19.0.10",
64
  "@types/react-dom": "^19.0.4",
65
+ "@types/react-i18next": "^8.1.0",
66
  "@types/react-syntax-highlighter": "^15.5.13",
67
  "@types/seedrandom": "^3.0.8",
68
  "@vitejs/plugin-react-swc": "^3.8.0",
 
444
 
445
  "@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="],
446
 
447
+ "@types/react-i18next": ["@types/[email protected]", "", { "dependencies": { "react-i18next": "*" } }, "sha512-d4xhcjX5b3roNMObRNMfb1HinHQlQLPo8xlDj60dnHeeAw2bBymR2cy/l1giJpHzo/ZFgSvgVUvIWr4kCrenCg=="],
448
+
449
  "@types/react-syntax-highlighter": ["@types/[email protected]", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="],
450
 
451
  "@types/react-transition-group": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
 
770
 
771
  "hoist-non-react-statics": ["[email protected]", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
772
 
773
+ "html-parse-stringify": ["[email protected]", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
774
+
775
  "html-url-attributes": ["[email protected]", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
776
 
777
+ "i18next": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.23.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ=="],
778
+
779
  "ignore": ["[email protected]", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
780
 
781
  "import-fresh": ["[email protected]", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
 
1102
 
1103
  "react-dropzone": ["[email protected]", "", { "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=="],
1104
 
1105
+ "react-i18next": ["[email protected]", "", { "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=="],
1106
+
1107
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
1108
 
1109
  "react-markdown": ["[email protected]", "", { "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=="],
 
1282
 
1283
  "vite": ["[email protected]", "", { "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=="],
1284
 
1285
+ "void-elements": ["[email protected]", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
1286
+
1287
  "which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
1288
 
1289
  "which-boxed-primitive": ["[email protected]", "", { "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=="],
lightrag_webui/package.json CHANGED
@@ -43,11 +43,13 @@
43
  "cmdk": "^1.0.4",
44
  "graphology": "^0.26.0",
45
  "graphology-generators": "^0.11.2",
 
46
  "lucide-react": "^0.475.0",
47
  "minisearch": "^7.1.2",
48
  "react": "^19.0.0",
49
  "react-dom": "^19.0.0",
50
  "react-dropzone": "^14.3.6",
 
51
  "react-markdown": "^9.1.0",
52
  "react-number-format": "^5.4.3",
53
  "react-syntax-highlighter": "^15.6.1",
@@ -69,6 +71,7 @@
69
  "@types/node": "^22.13.5",
70
  "@types/react": "^19.0.10",
71
  "@types/react-dom": "^19.0.4",
 
72
  "@types/react-syntax-highlighter": "^15.5.13",
73
  "@types/seedrandom": "^3.0.8",
74
  "@vitejs/plugin-react-swc": "^3.8.0",
 
43
  "cmdk": "^1.0.4",
44
  "graphology": "^0.26.0",
45
  "graphology-generators": "^0.11.2",
46
+ "i18next": "^24.2.2",
47
  "lucide-react": "^0.475.0",
48
  "minisearch": "^7.1.2",
49
  "react": "^19.0.0",
50
  "react-dom": "^19.0.0",
51
  "react-dropzone": "^14.3.6",
52
+ "react-i18next": "^15.4.1",
53
  "react-markdown": "^9.1.0",
54
  "react-number-format": "^5.4.3",
55
  "react-syntax-highlighter": "^15.6.1",
 
71
  "@types/node": "^22.13.5",
72
  "@types/react": "^19.0.10",
73
  "@types/react-dom": "^19.0.4",
74
+ "@types/react-i18next": "^8.1.0",
75
  "@types/react-syntax-highlighter": "^15.5.13",
76
  "@types/seedrandom": "^3.0.8",
77
  "@vitejs/plugin-react-swc": "^3.8.0",
lightrag_webui/src/components/ThemeToggle.tsx CHANGED
@@ -3,6 +3,7 @@ import useTheme from '@/hooks/useTheme'
3
  import { MoonIcon, SunIcon } from 'lucide-react'
4
  import { useCallback } from 'react'
5
  import { controlButtonVariant } from '@/lib/constants'
 
6
 
7
  /**
8
  * Component that toggles the theme between light and dark.
@@ -11,13 +12,14 @@ export default function ThemeToggle() {
11
  const { theme, setTheme } = useTheme()
12
  const setLight = useCallback(() => setTheme('light'), [setTheme])
13
  const setDark = useCallback(() => setTheme('dark'), [setTheme])
 
14
 
15
  if (theme === 'dark') {
16
  return (
17
  <Button
18
  onClick={setLight}
19
  variant={controlButtonVariant}
20
- tooltip="Switch to light theme"
21
  size="icon"
22
  side="bottom"
23
  >
@@ -29,7 +31,7 @@ export default function ThemeToggle() {
29
  <Button
30
  onClick={setDark}
31
  variant={controlButtonVariant}
32
- tooltip="Switch to dark theme"
33
  size="icon"
34
  side="bottom"
35
  >
 
3
  import { MoonIcon, SunIcon } from 'lucide-react'
4
  import { useCallback } from 'react'
5
  import { controlButtonVariant } from '@/lib/constants'
6
+ import { useTranslation } from 'react-i18next'
7
 
8
  /**
9
  * Component that toggles the theme between light and dark.
 
12
  const { theme, setTheme } = useTheme()
13
  const setLight = useCallback(() => setTheme('light'), [setTheme])
14
  const setDark = useCallback(() => setTheme('dark'), [setTheme])
15
+ const { t } = useTranslation()
16
 
17
  if (theme === 'dark') {
18
  return (
19
  <Button
20
  onClick={setLight}
21
  variant={controlButtonVariant}
22
+ tooltip={t('header.themeToggle.switchToLight')}
23
  size="icon"
24
  side="bottom"
25
  >
 
31
  <Button
32
  onClick={setDark}
33
  variant={controlButtonVariant}
34
+ tooltip={t('header.themeToggle.switchToDark')}
35
  size="icon"
36
  side="bottom"
37
  >
lightrag_webui/src/components/documents/ClearDocumentsDialog.tsx CHANGED
@@ -13,38 +13,40 @@ import { errorMessage } from '@/lib/utils'
13
  import { clearDocuments } from '@/api/lightrag'
14
 
15
  import { EraserIcon } from 'lucide-react'
 
16
 
17
  export default function ClearDocumentsDialog() {
 
18
  const [open, setOpen] = useState(false)
19
 
20
  const handleClear = useCallback(async () => {
21
  try {
22
  const result = await clearDocuments()
23
  if (result.status === 'success') {
24
- toast.success('Documents cleared successfully')
25
  setOpen(false)
26
  } else {
27
- toast.error(`Clear Documents Failed:\n${result.message}`)
28
  }
29
  } catch (err) {
30
- toast.error('Clear Documents Failed:\n' + errorMessage(err))
31
  }
32
  }, [setOpen])
33
 
34
  return (
35
  <Dialog open={open} onOpenChange={setOpen}>
36
  <DialogTrigger asChild>
37
- <Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
38
- <EraserIcon/> Clear
39
  </Button>
40
  </DialogTrigger>
41
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
42
  <DialogHeader>
43
- <DialogTitle>Clear documents</DialogTitle>
44
- <DialogDescription>Do you really want to clear all documents?</DialogDescription>
45
  </DialogHeader>
46
  <Button variant="destructive" onClick={handleClear}>
47
- YES
48
  </Button>
49
  </DialogContent>
50
  </Dialog>
 
13
  import { clearDocuments } from '@/api/lightrag'
14
 
15
  import { EraserIcon } from 'lucide-react'
16
+ import { useTranslation } from 'react-i18next'
17
 
18
  export default function ClearDocumentsDialog() {
19
+ const { t } = useTranslation()
20
  const [open, setOpen] = useState(false)
21
 
22
  const handleClear = useCallback(async () => {
23
  try {
24
  const result = await clearDocuments()
25
  if (result.status === 'success') {
26
+ toast.success(t('documentPanel.clearDocuments.success'))
27
  setOpen(false)
28
  } else {
29
+ toast.error(t('documentPanel.clearDocuments.failed', { message: result.message }))
30
  }
31
  } catch (err) {
32
+ toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
33
  }
34
  }, [setOpen])
35
 
36
  return (
37
  <Dialog open={open} onOpenChange={setOpen}>
38
  <DialogTrigger asChild>
39
+ <Button variant="outline" side="bottom" tooltip={t('documentPanel.clearDocuments.tooltip')} size="sm">
40
+ <EraserIcon/> {t('documentPanel.clearDocuments.button')}
41
  </Button>
42
  </DialogTrigger>
43
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
44
  <DialogHeader>
45
+ <DialogTitle>{t('documentPanel.clearDocuments.title')}</DialogTitle>
46
+ <DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription>
47
  </DialogHeader>
48
  <Button variant="destructive" onClick={handleClear}>
49
+ {t('documentPanel.clearDocuments.confirmButton')}
50
  </Button>
51
  </DialogContent>
52
  </Dialog>
lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx CHANGED
@@ -14,8 +14,10 @@ import { errorMessage } from '@/lib/utils'
14
  import { uploadDocument } from '@/api/lightrag'
15
 
16
  import { UploadIcon } from 'lucide-react'
 
17
 
18
  export default function UploadDocumentsDialog() {
 
19
  const [open, setOpen] = useState(false)
20
  const [isUploading, setIsUploading] = useState(false)
21
  const [progresses, setProgresses] = useState<Record<string, number>>({})
@@ -29,24 +31,24 @@ export default function UploadDocumentsDialog() {
29
  filesToUpload.map(async (file) => {
30
  try {
31
  const result = await uploadDocument(file, (percentCompleted: number) => {
32
- console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
33
  setProgresses((pre) => ({
34
  ...pre,
35
  [file.name]: percentCompleted
36
  }))
37
  })
38
  if (result.status === 'success') {
39
- toast.success(`Upload Success:\n${file.name} uploaded successfully`)
40
  } else {
41
- toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
42
  }
43
  } catch (err) {
44
- toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
45
  }
46
  })
47
  )
48
  } catch (err) {
49
- toast.error('Upload Failed\n' + errorMessage(err))
50
  } finally {
51
  setIsUploading(false)
52
  // setOpen(false)
@@ -66,21 +68,21 @@ export default function UploadDocumentsDialog() {
66
  }}
67
  >
68
  <DialogTrigger asChild>
69
- <Button variant="default" side="bottom" tooltip="Upload documents" size="sm">
70
- <UploadIcon /> Upload
71
  </Button>
72
  </DialogTrigger>
73
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
74
  <DialogHeader>
75
- <DialogTitle>Upload documents</DialogTitle>
76
  <DialogDescription>
77
- Drag and drop your documents here or click to browse.
78
  </DialogDescription>
79
  </DialogHeader>
80
  <FileUploader
81
  maxFileCount={Infinity}
82
  maxSize={200 * 1024 * 1024}
83
- 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"
84
  onUpload={handleDocumentsUpload}
85
  progresses={progresses}
86
  disabled={isUploading}
 
14
  import { uploadDocument } from '@/api/lightrag'
15
 
16
  import { UploadIcon } from 'lucide-react'
17
+ import { useTranslation } from 'react-i18next'
18
 
19
  export default function UploadDocumentsDialog() {
20
+ const { t } = useTranslation()
21
  const [open, setOpen] = useState(false)
22
  const [isUploading, setIsUploading] = useState(false)
23
  const [progresses, setProgresses] = useState<Record<string, number>>({})
 
31
  filesToUpload.map(async (file) => {
32
  try {
33
  const result = await uploadDocument(file, (percentCompleted: number) => {
34
+ console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted }))
35
  setProgresses((pre) => ({
36
  ...pre,
37
  [file.name]: percentCompleted
38
  }))
39
  })
40
  if (result.status === 'success') {
41
+ toast.success(t('documentPanel.uploadDocuments.success', { name: file.name }))
42
  } else {
43
+ toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message }))
44
  }
45
  } catch (err) {
46
+ toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) }))
47
  }
48
  })
49
  )
50
  } catch (err) {
51
+ toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
52
  } finally {
53
  setIsUploading(false)
54
  // setOpen(false)
 
68
  }}
69
  >
70
  <DialogTrigger asChild>
71
+ <Button variant="default" side="bottom" tooltip={t('documentPanel.uploadDocuments.tooltip')} size="sm">
72
+ <UploadIcon /> {t('documentPanel.uploadDocuments.button')}
73
  </Button>
74
  </DialogTrigger>
75
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
76
  <DialogHeader>
77
+ <DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
78
  <DialogDescription>
79
+ {t('documentPanel.uploadDocuments.description')}
80
  </DialogDescription>
81
  </DialogHeader>
82
  <FileUploader
83
  maxFileCount={Infinity}
84
  maxSize={200 * 1024 * 1024}
85
+ description={t('documentPanel.uploadDocuments.fileTypes')}
86
  onUpload={handleDocumentsUpload}
87
  progresses={progresses}
88
  disabled={isUploading}
lightrag_webui/src/components/graph/FullScreenControl.tsx CHANGED
@@ -2,21 +2,23 @@ import { useFullScreen } from '@react-sigma/core'
2
  import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
3
  import { controlButtonVariant } from '@/lib/constants'
4
  import Button from '@/components/ui/Button'
 
5
 
6
  /**
7
  * Component that toggles full screen mode.
8
  */
9
  const FullScreenControl = () => {
10
  const { isFullScreen, toggle } = useFullScreen()
 
11
 
12
  return (
13
  <>
14
  {isFullScreen ? (
15
- <Button variant={controlButtonVariant} onClick={toggle} tooltip="Windowed" size="icon">
16
  <MinimizeIcon />
17
  </Button>
18
  ) : (
19
- <Button variant={controlButtonVariant} onClick={toggle} tooltip="Full Screen" size="icon">
20
  <MaximizeIcon />
21
  </Button>
22
  )}
 
2
  import { MaximizeIcon, MinimizeIcon } from 'lucide-react'
3
  import { controlButtonVariant } from '@/lib/constants'
4
  import Button from '@/components/ui/Button'
5
+ import { useTranslation } from 'react-i18next'
6
 
7
  /**
8
  * Component that toggles full screen mode.
9
  */
10
  const FullScreenControl = () => {
11
  const { isFullScreen, toggle } = useFullScreen()
12
+ const { t } = useTranslation()
13
 
14
  return (
15
  <>
16
  {isFullScreen ? (
17
+ <Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.windowed')} size="icon">
18
  <MinimizeIcon />
19
  </Button>
20
  ) : (
21
+ <Button variant={controlButtonVariant} onClick={toggle} tooltip={t('graphPanel.sideBar.fullScreenControl.fullScreen')} size="icon">
22
  <MaximizeIcon />
23
  </Button>
24
  )}
lightrag_webui/src/components/graph/GraphLabels.tsx CHANGED
@@ -4,8 +4,10 @@ import { useSettingsStore } from '@/stores/settings'
4
  import { useGraphStore } from '@/stores/graph'
5
  import { labelListLimit } from '@/lib/constants'
6
  import MiniSearch from 'minisearch'
 
7
 
8
  const GraphLabels = () => {
 
9
  const label = useSettingsStore.use.queryLabel()
10
  const graphLabels = useGraphStore.use.graphLabels()
11
 
@@ -45,7 +47,7 @@ const GraphLabels = () => {
45
 
46
  return result.length <= labelListLimit
47
  ? result
48
- : [...result.slice(0, labelListLimit), `And ${result.length - labelListLimit} others`]
49
  },
50
  [getSearchEngine]
51
  )
@@ -68,14 +70,14 @@ const GraphLabels = () => {
68
  className="ml-2"
69
  triggerClassName="max-h-8"
70
  searchInputClassName="max-h-8"
71
- triggerTooltip="Select query label"
72
  fetcher={fetchData}
73
  renderOption={(item) => <div>{item}</div>}
74
  getOptionValue={(item) => item}
75
  getDisplayValue={(item) => <div>{item}</div>}
76
  notFound={<div className="py-6 text-center text-sm">No labels found</div>}
77
- label="Label"
78
- placeholder="Search labels..."
79
  value={label !== null ? label : ''}
80
  onChange={setQueryLabel}
81
  clearable={false} // Prevent clearing value on reselect
 
4
  import { useGraphStore } from '@/stores/graph'
5
  import { labelListLimit } from '@/lib/constants'
6
  import MiniSearch from 'minisearch'
7
+ import { useTranslation } from 'react-i18next'
8
 
9
  const GraphLabels = () => {
10
+ const { t } = useTranslation()
11
  const label = useSettingsStore.use.queryLabel()
12
  const graphLabels = useGraphStore.use.graphLabels()
13
 
 
47
 
48
  return result.length <= labelListLimit
49
  ? result
50
+ : [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
51
  },
52
  [getSearchEngine]
53
  )
 
70
  className="ml-2"
71
  triggerClassName="max-h-8"
72
  searchInputClassName="max-h-8"
73
+ triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
74
  fetcher={fetchData}
75
  renderOption={(item) => <div>{item}</div>}
76
  getOptionValue={(item) => item}
77
  getDisplayValue={(item) => <div>{item}</div>}
78
  notFound={<div className="py-6 text-center text-sm">No labels found</div>}
79
+ label={t('graphPanel.graphLabels.label')}
80
+ placeholder={t('graphPanel.graphLabels.placeholder')}
81
  value={label !== null ? label : ''}
82
  onChange={setQueryLabel}
83
  clearable={false} // Prevent clearing value on reselect
lightrag_webui/src/components/graph/GraphSearch.tsx CHANGED
@@ -9,6 +9,7 @@ import { AsyncSearch } from '@/components/ui/AsyncSearch'
9
  import { searchResultLimit } from '@/lib/constants'
10
  import { useGraphStore } from '@/stores/graph'
11
  import MiniSearch from 'minisearch'
 
12
 
13
  interface OptionItem {
14
  id: string
@@ -44,6 +45,7 @@ export const GraphSearchInput = ({
44
  onFocus?: GraphSearchInputProps['onFocus']
45
  value?: GraphSearchInputProps['value']
46
  }) => {
 
47
  const graph = useGraphStore.use.sigmaGraph()
48
 
49
  const searchEngine = useMemo(() => {
@@ -97,7 +99,7 @@ export const GraphSearchInput = ({
97
  {
98
  type: 'message',
99
  id: messageId,
100
- message: `And ${result.length - searchResultLimit} others`
101
  }
102
  ]
103
  },
@@ -118,7 +120,7 @@ export const GraphSearchInput = ({
118
  if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
119
  }}
120
  label={'item'}
121
- placeholder="Search nodes..."
122
  />
123
  )
124
  }
 
9
  import { searchResultLimit } from '@/lib/constants'
10
  import { useGraphStore } from '@/stores/graph'
11
  import MiniSearch from 'minisearch'
12
+ import { useTranslation } from 'react-i18next'
13
 
14
  interface OptionItem {
15
  id: string
 
45
  onFocus?: GraphSearchInputProps['onFocus']
46
  value?: GraphSearchInputProps['value']
47
  }) => {
48
+ const { t } = useTranslation()
49
  const graph = useGraphStore.use.sigmaGraph()
50
 
51
  const searchEngine = useMemo(() => {
 
99
  {
100
  type: 'message',
101
  id: messageId,
102
+ message: t('graphPanel.search.message', { count: result.length - searchResultLimit })
103
  }
104
  ]
105
  },
 
120
  if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
121
  }}
122
  label={'item'}
123
+ placeholder={t('graphPanel.search.placeholder')}
124
  />
125
  )
126
  }
lightrag_webui/src/components/graph/LayoutsControl.tsx CHANGED
@@ -16,6 +16,7 @@ import { controlButtonVariant } from '@/lib/constants'
16
  import { useSettingsStore } from '@/stores/settings'
17
 
18
  import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
 
19
 
20
  type LayoutName =
21
  | 'Circular'
@@ -28,6 +29,7 @@ type LayoutName =
28
  const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
29
  const sigma = useSigma()
30
  const { stop, start, isRunning } = layout
 
31
 
32
  /**
33
  * Init component when Sigma or component settings change.
@@ -61,7 +63,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
61
  <Button
62
  size="icon"
63
  onClick={() => (isRunning ? stop() : start())}
64
- tooltip={isRunning ? 'Stop the layout animation' : 'Start the layout animation'}
65
  variant={controlButtonVariant}
66
  >
67
  {isRunning ? <PauseIcon /> : <PlayIcon />}
@@ -74,6 +76,7 @@ const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) =
74
  */
75
  const LayoutsControl = () => {
76
  const sigma = useSigma()
 
77
  const [layout, setLayout] = useState<LayoutName>('Circular')
78
  const [opened, setOpened] = useState<boolean>(false)
79
 
@@ -149,7 +152,7 @@ const LayoutsControl = () => {
149
  size="icon"
150
  variant={controlButtonVariant}
151
  onClick={() => setOpened((e: boolean) => !e)}
152
- tooltip="Layout Graph"
153
  >
154
  <GripIcon />
155
  </Button>
@@ -166,7 +169,7 @@ const LayoutsControl = () => {
166
  key={name}
167
  className="cursor-pointer text-xs"
168
  >
169
- {name}
170
  </CommandItem>
171
  ))}
172
  </CommandGroup>
 
16
  import { useSettingsStore } from '@/stores/settings'
17
 
18
  import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
19
+ import { useTranslation } from 'react-i18next'
20
 
21
  type LayoutName =
22
  | 'Circular'
 
29
  const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => {
30
  const sigma = useSigma()
31
  const { stop, start, isRunning } = layout
32
+ const { t } = useTranslation()
33
 
34
  /**
35
  * Init component when Sigma or component settings change.
 
63
  <Button
64
  size="icon"
65
  onClick={() => (isRunning ? stop() : start())}
66
+ tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
67
  variant={controlButtonVariant}
68
  >
69
  {isRunning ? <PauseIcon /> : <PlayIcon />}
 
76
  */
77
  const LayoutsControl = () => {
78
  const sigma = useSigma()
79
+ const { t } = useTranslation()
80
  const [layout, setLayout] = useState<LayoutName>('Circular')
81
  const [opened, setOpened] = useState<boolean>(false)
82
 
 
152
  size="icon"
153
  variant={controlButtonVariant}
154
  onClick={() => setOpened((e: boolean) => !e)}
155
+ tooltip={t('graphPanel.sideBar.layoutsControl.layoutGraph')}
156
  >
157
  <GripIcon />
158
  </Button>
 
169
  key={name}
170
  className="cursor-pointer text-xs"
171
  >
172
+ {t(`graphPanel.sideBar.layoutsControl.layouts.${name}`)}
173
  </CommandItem>
174
  ))}
175
  </CommandGroup>
lightrag_webui/src/components/graph/PropertiesView.tsx CHANGED
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
2
  import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
3
  import Text from '@/components/ui/Text'
4
  import useLightragGraph from '@/hooks/useLightragGraph'
 
5
 
6
  /**
7
  * Component that view properties of elements in graph.
@@ -147,21 +148,22 @@ const PropertyRow = ({
147
  }
148
 
149
  const NodePropertiesView = ({ node }: { node: NodeType }) => {
 
150
  return (
151
  <div className="flex flex-col gap-2">
152
- <label className="text-md pl-1 font-bold tracking-wide text-sky-300">Node</label>
153
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
154
- <PropertyRow name={'Id'} value={node.id} />
155
  <PropertyRow
156
- name={'Labels'}
157
  value={node.labels.join(', ')}
158
  onClick={() => {
159
  useGraphStore.getState().setSelectedNode(node.id, true)
160
  }}
161
  />
162
- <PropertyRow name={'Degree'} value={node.degree} />
163
  </div>
164
- <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
165
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
166
  {Object.keys(node.properties)
167
  .sort()
@@ -172,7 +174,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
172
  {node.relationships.length > 0 && (
173
  <>
174
  <label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
175
- Relationships
176
  </label>
177
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
178
  {node.relationships.map(({ type, id, label }) => {
@@ -195,28 +197,29 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
195
  }
196
 
197
  const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
 
198
  return (
199
  <div className="flex flex-col gap-2">
200
- <label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
201
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
202
- <PropertyRow name={'Id'} value={edge.id} />
203
- {edge.type && <PropertyRow name={'Type'} value={edge.type} />}
204
  <PropertyRow
205
- name={'Source'}
206
  value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
207
  onClick={() => {
208
  useGraphStore.getState().setSelectedNode(edge.source, true)
209
  }}
210
  />
211
  <PropertyRow
212
- name={'Target'}
213
  value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
214
  onClick={() => {
215
  useGraphStore.getState().setSelectedNode(edge.target, true)
216
  }}
217
  />
218
  </div>
219
- <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">Properties</label>
220
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
221
  {Object.keys(edge.properties)
222
  .sort()
 
2
  import { useGraphStore, RawNodeType, RawEdgeType } from '@/stores/graph'
3
  import Text from '@/components/ui/Text'
4
  import useLightragGraph from '@/hooks/useLightragGraph'
5
+ import { useTranslation } from 'react-i18next'
6
 
7
  /**
8
  * Component that view properties of elements in graph.
 
148
  }
149
 
150
  const NodePropertiesView = ({ node }: { node: NodeType }) => {
151
+ const { t } = useTranslation()
152
  return (
153
  <div className="flex flex-col gap-2">
154
+ <label className="text-md pl-1 font-bold tracking-wide text-sky-300">{t('graphPanel.propertiesView.node.title')}</label>
155
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
156
+ <PropertyRow name={t('graphPanel.propertiesView.node.id')} value={node.id} />
157
  <PropertyRow
158
+ name={t('graphPanel.propertiesView.node.labels')}
159
  value={node.labels.join(', ')}
160
  onClick={() => {
161
  useGraphStore.getState().setSelectedNode(node.id, true)
162
  }}
163
  />
164
+ <PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
165
  </div>
166
+ <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.node.properties')}</label>
167
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
168
  {Object.keys(node.properties)
169
  .sort()
 
174
  {node.relationships.length > 0 && (
175
  <>
176
  <label className="text-md pl-1 font-bold tracking-wide text-teal-600/90">
177
+ {t('graphPanel.propertiesView.node.relationships')}
178
  </label>
179
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
180
  {node.relationships.map(({ type, id, label }) => {
 
197
  }
198
 
199
  const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
200
+ const { t } = useTranslation()
201
  return (
202
  <div className="flex flex-col gap-2">
203
+ <label className="text-md pl-1 font-bold tracking-wide text-teal-600">{t('graphPanel.propertiesView.edge.title')}</label>
204
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
205
+ <PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
206
+ {edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
207
  <PropertyRow
208
+ name={t('graphPanel.propertiesView.edge.source')}
209
  value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}
210
  onClick={() => {
211
  useGraphStore.getState().setSelectedNode(edge.source, true)
212
  }}
213
  />
214
  <PropertyRow
215
+ name={t('graphPanel.propertiesView.edge.target')}
216
  value={edge.targetNode ? edge.targetNode.labels.join(', ') : edge.target}
217
  onClick={() => {
218
  useGraphStore.getState().setSelectedNode(edge.target, true)
219
  }}
220
  />
221
  </div>
222
+ <label className="text-md pl-1 font-bold tracking-wide text-yellow-400/90">{t('graphPanel.propertiesView.edge.properties')}</label>
223
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
224
  {Object.keys(edge.properties)
225
  .sort()
lightrag_webui/src/components/graph/Settings.tsx CHANGED
@@ -10,6 +10,7 @@ import { useSettingsStore } from '@/stores/settings'
10
  import { useBackendState } from '@/stores/state'
11
 
12
  import { SettingsIcon, RefreshCwIcon } from 'lucide-react'
 
13
 
14
  /**
15
  * Component that displays a checkbox with a label.
@@ -205,11 +206,13 @@ export default function Settings() {
205
  [setTempApiKey]
206
  )
207
 
 
 
208
  return (
209
  <>
210
  <Button
211
  variant={controlButtonVariant}
212
- tooltip="Refresh Layout"
213
  size="icon"
214
  onClick={refreshLayout}
215
  >
@@ -217,7 +220,7 @@ export default function Settings() {
217
  </Button>
218
  <Popover open={opened} onOpenChange={setOpened}>
219
  <PopoverTrigger asChild>
220
- <Button variant={controlButtonVariant} tooltip="Settings" size="icon">
221
  <SettingsIcon />
222
  </Button>
223
  </PopoverTrigger>
@@ -231,7 +234,7 @@ export default function Settings() {
231
  <LabeledCheckBox
232
  checked={enableHealthCheck}
233
  onCheckedChange={setEnableHealthCheck}
234
- label="Health Check"
235
  />
236
 
237
  <Separator />
@@ -239,12 +242,12 @@ export default function Settings() {
239
  <LabeledCheckBox
240
  checked={showPropertyPanel}
241
  onCheckedChange={setShowPropertyPanel}
242
- label="Show Property Panel"
243
  />
244
  <LabeledCheckBox
245
  checked={showNodeSearchBar}
246
  onCheckedChange={setShowNodeSearchBar}
247
- label="Show Search Bar"
248
  />
249
 
250
  <Separator />
@@ -252,12 +255,12 @@ export default function Settings() {
252
  <LabeledCheckBox
253
  checked={showNodeLabel}
254
  onCheckedChange={setShowNodeLabel}
255
- label="Show Node Label"
256
  />
257
  <LabeledCheckBox
258
  checked={enableNodeDrag}
259
  onCheckedChange={setEnableNodeDrag}
260
- label="Node Draggable"
261
  />
262
 
263
  <Separator />
@@ -265,51 +268,50 @@ export default function Settings() {
265
  <LabeledCheckBox
266
  checked={showEdgeLabel}
267
  onCheckedChange={setShowEdgeLabel}
268
- label="Show Edge Label"
269
  />
270
  <LabeledCheckBox
271
  checked={enableHideUnselectedEdges}
272
  onCheckedChange={setEnableHideUnselectedEdges}
273
- label="Hide Unselected Edges"
274
  />
275
  <LabeledCheckBox
276
  checked={enableEdgeEvents}
277
  onCheckedChange={setEnableEdgeEvents}
278
- label="Edge Events"
279
  />
280
 
281
  <Separator />
282
  <LabeledNumberInput
283
- label="Max Query Depth"
284
  min={1}
285
  value={graphQueryMaxDepth}
286
  onEditFinished={setGraphQueryMaxDepth}
287
  />
288
  <LabeledNumberInput
289
- label="Minimum Degree"
290
  min={0}
291
  value={graphMinDegree}
292
  onEditFinished={setGraphMinDegree}
293
  />
294
  <LabeledNumberInput
295
- label="Max Layout Iterations"
296
  min={1}
297
  max={30}
298
  value={graphLayoutMaxIterations}
299
  onEditFinished={setGraphLayoutMaxIterations}
300
  />
301
-
302
  <Separator />
303
 
304
  <div className="flex flex-col gap-2">
305
- <label className="text-sm font-medium">API Key</label>
306
  <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
307
  <div className="w-0 flex-1">
308
  <Input
309
  type="password"
310
  value={tempApiKey}
311
  onChange={handleTempApiKeyChange}
312
- placeholder="Enter your API key"
313
  className="max-h-full w-full min-w-0"
314
  autoComplete="off"
315
  />
@@ -320,7 +322,7 @@ export default function Settings() {
320
  size="sm"
321
  className="max-h-full shrink-0"
322
  >
323
- Save
324
  </Button>
325
  </form>
326
  </div>
 
10
  import { useBackendState } from '@/stores/state'
11
 
12
  import { SettingsIcon, RefreshCwIcon } from 'lucide-react'
13
+ import { useTranslation } from 'react-i18next';
14
 
15
  /**
16
  * Component that displays a checkbox with a label.
 
206
  [setTempApiKey]
207
  )
208
 
209
+ const { t } = useTranslation();
210
+
211
  return (
212
  <>
213
  <Button
214
  variant={controlButtonVariant}
215
+ tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
216
  size="icon"
217
  onClick={refreshLayout}
218
  >
 
220
  </Button>
221
  <Popover open={opened} onOpenChange={setOpened}>
222
  <PopoverTrigger asChild>
223
+ <Button variant={controlButtonVariant} tooltip={t('graphPanel.sideBar.settings.settings')} size="icon">
224
  <SettingsIcon />
225
  </Button>
226
  </PopoverTrigger>
 
234
  <LabeledCheckBox
235
  checked={enableHealthCheck}
236
  onCheckedChange={setEnableHealthCheck}
237
+ label={t('graphPanel.sideBar.settings.healthCheck')}
238
  />
239
 
240
  <Separator />
 
242
  <LabeledCheckBox
243
  checked={showPropertyPanel}
244
  onCheckedChange={setShowPropertyPanel}
245
+ label={t('graphPanel.sideBar.settings.showPropertyPanel')}
246
  />
247
  <LabeledCheckBox
248
  checked={showNodeSearchBar}
249
  onCheckedChange={setShowNodeSearchBar}
250
+ label={t('graphPanel.sideBar.settings.showSearchBar')}
251
  />
252
 
253
  <Separator />
 
255
  <LabeledCheckBox
256
  checked={showNodeLabel}
257
  onCheckedChange={setShowNodeLabel}
258
+ label={t('graphPanel.sideBar.settings.showNodeLabel')}
259
  />
260
  <LabeledCheckBox
261
  checked={enableNodeDrag}
262
  onCheckedChange={setEnableNodeDrag}
263
+ label={t('graphPanel.sideBar.settings.nodeDraggable')}
264
  />
265
 
266
  <Separator />
 
268
  <LabeledCheckBox
269
  checked={showEdgeLabel}
270
  onCheckedChange={setShowEdgeLabel}
271
+ label={t('graphPanel.sideBar.settings.showEdgeLabel')}
272
  />
273
  <LabeledCheckBox
274
  checked={enableHideUnselectedEdges}
275
  onCheckedChange={setEnableHideUnselectedEdges}
276
+ label={t('graphPanel.sideBar.settings.hideUnselectedEdges')}
277
  />
278
  <LabeledCheckBox
279
  checked={enableEdgeEvents}
280
  onCheckedChange={setEnableEdgeEvents}
281
+ label={t('graphPanel.sideBar.settings.edgeEvents')}
282
  />
283
 
284
  <Separator />
285
  <LabeledNumberInput
286
+ label={t('graphPanel.sideBar.settings.maxQueryDepth')}
287
  min={1}
288
  value={graphQueryMaxDepth}
289
  onEditFinished={setGraphQueryMaxDepth}
290
  />
291
  <LabeledNumberInput
292
+ label={t('graphPanel.sideBar.settings.minDegree')}
293
  min={0}
294
  value={graphMinDegree}
295
  onEditFinished={setGraphMinDegree}
296
  />
297
  <LabeledNumberInput
298
+ label={t('graphPanel.sideBar.settings.maxLayoutIterations')}
299
  min={1}
300
  max={30}
301
  value={graphLayoutMaxIterations}
302
  onEditFinished={setGraphLayoutMaxIterations}
303
  />
 
304
  <Separator />
305
 
306
  <div className="flex flex-col gap-2">
307
+ <label className="text-sm font-medium">{t('graphPanel.sideBar.settings.apiKey')}</label>
308
  <form className="flex h-6 gap-2" onSubmit={(e) => e.preventDefault()}>
309
  <div className="w-0 flex-1">
310
  <Input
311
  type="password"
312
  value={tempApiKey}
313
  onChange={handleTempApiKeyChange}
314
+ placeholder={t('graphPanel.sideBar.settings.enterYourAPIkey')}
315
  className="max-h-full w-full min-w-0"
316
  autoComplete="off"
317
  />
 
322
  size="sm"
323
  className="max-h-full shrink-0"
324
  >
325
+ {t('graphPanel.sideBar.settings.save')}
326
  </Button>
327
  </form>
328
  </div>
lightrag_webui/src/components/graph/StatusCard.tsx CHANGED
@@ -1,58 +1,60 @@
1
  import { LightragStatus } from '@/api/lightrag'
 
2
 
3
  const StatusCard = ({ status }: { status: LightragStatus | null }) => {
 
4
  if (!status) {
5
- return <div className="text-muted-foreground text-sm">Status information unavailable</div>
6
  }
7
 
8
  return (
9
  <div className="min-w-[300px] space-y-3 text-sm">
10
  <div className="space-y-1">
11
- <h4 className="font-medium">Storage Info</h4>
12
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
13
- <span>Working Directory:</span>
14
  <span className="truncate">{status.working_directory}</span>
15
- <span>Input Directory:</span>
16
  <span className="truncate">{status.input_directory}</span>
17
  </div>
18
  </div>
19
 
20
  <div className="space-y-1">
21
- <h4 className="font-medium">LLM Configuration</h4>
22
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
23
- <span>LLM Binding:</span>
24
  <span>{status.configuration.llm_binding}</span>
25
- <span>LLM Binding Host:</span>
26
  <span>{status.configuration.llm_binding_host}</span>
27
- <span>LLM Model:</span>
28
  <span>{status.configuration.llm_model}</span>
29
- <span>Max Tokens:</span>
30
  <span>{status.configuration.max_tokens}</span>
31
  </div>
32
  </div>
33
 
34
  <div className="space-y-1">
35
- <h4 className="font-medium">Embedding Configuration</h4>
36
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
37
- <span>Embedding Binding:</span>
38
  <span>{status.configuration.embedding_binding}</span>
39
- <span>Embedding Binding Host:</span>
40
  <span>{status.configuration.embedding_binding_host}</span>
41
- <span>Embedding Model:</span>
42
  <span>{status.configuration.embedding_model}</span>
43
  </div>
44
  </div>
45
 
46
  <div className="space-y-1">
47
- <h4 className="font-medium">Storage Configuration</h4>
48
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
49
- <span>KV Storage:</span>
50
  <span>{status.configuration.kv_storage}</span>
51
- <span>Doc Status Storage:</span>
52
  <span>{status.configuration.doc_status_storage}</span>
53
- <span>Graph Storage:</span>
54
  <span>{status.configuration.graph_storage}</span>
55
- <span>Vector Storage:</span>
56
  <span>{status.configuration.vector_storage}</span>
57
  </div>
58
  </div>
 
1
  import { LightragStatus } from '@/api/lightrag'
2
+ import { useTranslation } from 'react-i18next'
3
 
4
  const StatusCard = ({ status }: { status: LightragStatus | null }) => {
5
+ const { t } = useTranslation()
6
  if (!status) {
7
+ return <div className="text-muted-foreground text-sm">{t('graphPanel.statusCard.unavailable')}</div>
8
  }
9
 
10
  return (
11
  <div className="min-w-[300px] space-y-3 text-sm">
12
  <div className="space-y-1">
13
+ <h4 className="font-medium">{t('graphPanel.statusCard.storageInfo')}</h4>
14
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
15
+ <span>{t('graphPanel.statusCard.workingDirectory')}:</span>
16
  <span className="truncate">{status.working_directory}</span>
17
+ <span>{t('graphPanel.statusCard.inputDirectory')}:</span>
18
  <span className="truncate">{status.input_directory}</span>
19
  </div>
20
  </div>
21
 
22
  <div className="space-y-1">
23
+ <h4 className="font-medium">{t('graphPanel.statusCard.llmConfig')}</h4>
24
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
25
+ <span>{t('graphPanel.statusCard.llmBinding')}:</span>
26
  <span>{status.configuration.llm_binding}</span>
27
+ <span>{t('graphPanel.statusCard.llmBindingHost')}:</span>
28
  <span>{status.configuration.llm_binding_host}</span>
29
+ <span>{t('graphPanel.statusCard.llmModel')}:</span>
30
  <span>{status.configuration.llm_model}</span>
31
+ <span>{t('graphPanel.statusCard.maxTokens')}:</span>
32
  <span>{status.configuration.max_tokens}</span>
33
  </div>
34
  </div>
35
 
36
  <div className="space-y-1">
37
+ <h4 className="font-medium">{t('graphPanel.statusCard.embeddingConfig')}</h4>
38
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
39
+ <span>{t('graphPanel.statusCard.embeddingBinding')}:</span>
40
  <span>{status.configuration.embedding_binding}</span>
41
+ <span>{t('graphPanel.statusCard.embeddingBindingHost')}:</span>
42
  <span>{status.configuration.embedding_binding_host}</span>
43
+ <span>{t('graphPanel.statusCard.embeddingModel')}:</span>
44
  <span>{status.configuration.embedding_model}</span>
45
  </div>
46
  </div>
47
 
48
  <div className="space-y-1">
49
+ <h4 className="font-medium">{t('graphPanel.statusCard.storageConfig')}</h4>
50
  <div className="text-muted-foreground grid grid-cols-2 gap-1">
51
+ <span>{t('graphPanel.statusCard.kvStorage')}:</span>
52
  <span>{status.configuration.kv_storage}</span>
53
+ <span>{t('graphPanel.statusCard.docStatusStorage')}:</span>
54
  <span>{status.configuration.doc_status_storage}</span>
55
+ <span>{t('graphPanel.statusCard.graphStorage')}:</span>
56
  <span>{status.configuration.graph_storage}</span>
57
+ <span>{t('graphPanel.statusCard.vectorStorage')}:</span>
58
  <span>{status.configuration.vector_storage}</span>
59
  </div>
60
  </div>
lightrag_webui/src/components/graph/StatusIndicator.tsx CHANGED
@@ -3,8 +3,10 @@ import { useBackendState } from '@/stores/state'
3
  import { useEffect, useState } from 'react'
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
5
  import StatusCard from '@/components/graph/StatusCard'
 
6
 
7
  const StatusIndicator = () => {
 
8
  const health = useBackendState.use.health()
9
  const lastCheckTime = useBackendState.use.lastCheckTime()
10
  const status = useBackendState.use.status()
@@ -33,7 +35,7 @@ const StatusIndicator = () => {
33
  )}
34
  />
35
  <span className="text-muted-foreground text-xs">
36
- {health ? 'Connected' : 'Disconnected'}
37
  </span>
38
  </div>
39
  </PopoverTrigger>
 
3
  import { useEffect, useState } from 'react'
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
5
  import StatusCard from '@/components/graph/StatusCard'
6
+ import { useTranslation } from 'react-i18next'
7
 
8
  const StatusIndicator = () => {
9
+ const { t } = useTranslation()
10
  const health = useBackendState.use.health()
11
  const lastCheckTime = useBackendState.use.lastCheckTime()
12
  const status = useBackendState.use.status()
 
35
  )}
36
  />
37
  <span className="text-muted-foreground text-xs">
38
+ {health ? t('graphPanel.statusIndicator.connected') : t('graphPanel.statusIndicator.disconnected')}
39
  </span>
40
  </div>
41
  </PopoverTrigger>
lightrag_webui/src/components/graph/ZoomControl.tsx CHANGED
@@ -3,12 +3,14 @@ import { useCallback } from 'react'
3
  import Button from '@/components/ui/Button'
4
  import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
5
  import { controlButtonVariant } from '@/lib/constants'
 
6
 
7
  /**
8
  * Component that provides zoom controls for the graph viewer.
9
  */
10
  const ZoomControl = () => {
11
  const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
 
12
 
13
  const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
14
  const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
@@ -16,16 +18,16 @@ const ZoomControl = () => {
16
 
17
  return (
18
  <>
19
- <Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip="Zoom In" size="icon">
20
  <ZoomInIcon />
21
  </Button>
22
- <Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip="Zoom Out" size="icon">
23
  <ZoomOutIcon />
24
  </Button>
25
  <Button
26
  variant={controlButtonVariant}
27
  onClick={handleResetZoom}
28
- tooltip="Reset Zoom"
29
  size="icon"
30
  >
31
  <FullscreenIcon />
 
3
  import Button from '@/components/ui/Button'
4
  import { ZoomInIcon, ZoomOutIcon, FullscreenIcon } from 'lucide-react'
5
  import { controlButtonVariant } from '@/lib/constants'
6
+ import { useTranslation } from "react-i18next";
7
 
8
  /**
9
  * Component that provides zoom controls for the graph viewer.
10
  */
11
  const ZoomControl = () => {
12
  const { zoomIn, zoomOut, reset } = useCamera({ duration: 200, factor: 1.5 })
13
+ const { t } = useTranslation();
14
 
15
  const handleZoomIn = useCallback(() => zoomIn(), [zoomIn])
16
  const handleZoomOut = useCallback(() => zoomOut(), [zoomOut])
 
18
 
19
  return (
20
  <>
21
+ <Button variant={controlButtonVariant} onClick={handleZoomIn} tooltip={t("graphPanel.sideBar.zoomControl.zoomIn")} size="icon">
22
  <ZoomInIcon />
23
  </Button>
24
+ <Button variant={controlButtonVariant} onClick={handleZoomOut} tooltip={t("graphPanel.sideBar.zoomControl.zoomOut")} size="icon">
25
  <ZoomOutIcon />
26
  </Button>
27
  <Button
28
  variant={controlButtonVariant}
29
  onClick={handleResetZoom}
30
+ tooltip={t("graphPanel.sideBar.zoomControl.resetZoom")}
31
  size="icon"
32
  >
33
  <FullscreenIcon />
lightrag_webui/src/components/retrieval/ChatMessage.tsx CHANGED
@@ -15,18 +15,21 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
15
  import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
16
 
17
  import { LoaderIcon, CopyIcon } from 'lucide-react'
 
18
 
19
  export type MessageWithError = Message & {
20
  isError?: boolean
21
  }
22
 
23
  export const ChatMessage = ({ message }: { message: MessageWithError }) => {
 
 
24
  const handleCopyMarkdown = useCallback(async () => {
25
  if (message.content) {
26
  try {
27
  await navigator.clipboard.writeText(message.content)
28
  } catch (err) {
29
- console.error('Failed to copy:', err)
30
  }
31
  }
32
  }, [message])
@@ -57,7 +60,7 @@ export const ChatMessage = ({ message }: { message: MessageWithError }) => {
57
  <Button
58
  onClick={handleCopyMarkdown}
59
  className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
60
- tooltip="Copy to clipboard"
61
  variant="default"
62
  size="icon"
63
  >
 
15
  import { oneLight, oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
16
 
17
  import { LoaderIcon, CopyIcon } from 'lucide-react'
18
+ import { useTranslation } from 'react-i18next'
19
 
20
  export type MessageWithError = Message & {
21
  isError?: boolean
22
  }
23
 
24
  export const ChatMessage = ({ message }: { message: MessageWithError }) => {
25
+ const { t } = useTranslation()
26
+
27
  const handleCopyMarkdown = useCallback(async () => {
28
  if (message.content) {
29
  try {
30
  await navigator.clipboard.writeText(message.content)
31
  } catch (err) {
32
+ console.error(t('chat.copyError'), err)
33
  }
34
  }
35
  }, [message])
 
60
  <Button
61
  onClick={handleCopyMarkdown}
62
  className="absolute right-0 bottom-0 size-6 rounded-md opacity-20 transition-opacity hover:opacity-100"
63
+ tooltip={t('retrievePanel.chatMessage.copyTooltip')}
64
  variant="default"
65
  size="icon"
66
  >
lightrag_webui/src/components/retrieval/QuerySettings.tsx CHANGED
@@ -14,8 +14,10 @@ import {
14
  SelectValue
15
  } from '@/components/ui/Select'
16
  import { useSettingsStore } from '@/stores/settings'
 
17
 
18
  export default function QuerySettings() {
 
19
  const querySettings = useSettingsStore((state) => state.querySettings)
20
 
21
  const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
@@ -25,8 +27,8 @@ export default function QuerySettings() {
25
  return (
26
  <Card className="flex shrink-0 flex-col">
27
  <CardHeader className="px-4 pt-4 pb-2">
28
- <CardTitle>Parameters</CardTitle>
29
- <CardDescription>Configure your query parameters</CardDescription>
30
  </CardHeader>
31
  <CardContent className="m-0 flex grow flex-col p-0 text-xs">
32
  <div className="relative size-full">
@@ -35,8 +37,8 @@ export default function QuerySettings() {
35
  <>
36
  <Text
37
  className="ml-1"
38
- text="Query Mode"
39
- 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"
40
  side="left"
41
  />
42
  <Select
@@ -48,11 +50,11 @@ export default function QuerySettings() {
48
  </SelectTrigger>
49
  <SelectContent>
50
  <SelectGroup>
51
- <SelectItem value="naive">Naive</SelectItem>
52
- <SelectItem value="local">Local</SelectItem>
53
- <SelectItem value="global">Global</SelectItem>
54
- <SelectItem value="hybrid">Hybrid</SelectItem>
55
- <SelectItem value="mix">Mix</SelectItem>
56
  </SelectGroup>
57
  </SelectContent>
58
  </Select>
@@ -62,8 +64,8 @@ export default function QuerySettings() {
62
  <>
63
  <Text
64
  className="ml-1"
65
- text="Response Format"
66
- tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
67
  side="left"
68
  />
69
  <Select
@@ -75,9 +77,9 @@ export default function QuerySettings() {
75
  </SelectTrigger>
76
  <SelectContent>
77
  <SelectGroup>
78
- <SelectItem value="Multiple Paragraphs">Multiple Paragraphs</SelectItem>
79
- <SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
80
- <SelectItem value="Bullet Points">Bullet Points</SelectItem>
81
  </SelectGroup>
82
  </SelectContent>
83
  </Select>
@@ -87,8 +89,8 @@ export default function QuerySettings() {
87
  <>
88
  <Text
89
  className="ml-1"
90
- text="Top K Results"
91
- tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
92
  side="left"
93
  />
94
  <NumberInput
@@ -97,7 +99,7 @@ export default function QuerySettings() {
97
  value={querySettings.top_k}
98
  onValueChange={(v) => handleChange('top_k', v)}
99
  min={1}
100
- placeholder="Number of results"
101
  />
102
  </>
103
 
@@ -106,8 +108,8 @@ export default function QuerySettings() {
106
  <>
107
  <Text
108
  className="ml-1"
109
- text="Max Tokens for Text Unit"
110
- tooltip="Maximum number of tokens allowed for each retrieved text chunk"
111
  side="left"
112
  />
113
  <NumberInput
@@ -116,14 +118,14 @@ export default function QuerySettings() {
116
  value={querySettings.max_token_for_text_unit}
117
  onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
118
  min={1}
119
- placeholder="Max tokens for text unit"
120
  />
121
  </>
122
 
123
  <>
124
  <Text
125
- text="Max Tokens for Global Context"
126
- tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
127
  side="left"
128
  />
129
  <NumberInput
@@ -132,15 +134,15 @@ export default function QuerySettings() {
132
  value={querySettings.max_token_for_global_context}
133
  onValueChange={(v) => handleChange('max_token_for_global_context', v)}
134
  min={1}
135
- placeholder="Max tokens for global context"
136
  />
137
  </>
138
 
139
  <>
140
  <Text
141
  className="ml-1"
142
- text="Max Tokens for Local Context"
143
- tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
144
  side="left"
145
  />
146
  <NumberInput
@@ -149,7 +151,7 @@ export default function QuerySettings() {
149
  value={querySettings.max_token_for_local_context}
150
  onValueChange={(v) => handleChange('max_token_for_local_context', v)}
151
  min={1}
152
- placeholder="Max tokens for local context"
153
  />
154
  </>
155
  </>
@@ -158,8 +160,8 @@ export default function QuerySettings() {
158
  <>
159
  <Text
160
  className="ml-1"
161
- text="History Turns"
162
- tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
163
  side="left"
164
  />
165
  <NumberInput
@@ -170,7 +172,7 @@ export default function QuerySettings() {
170
  value={querySettings.history_turns}
171
  onValueChange={(v) => handleChange('history_turns', v)}
172
  min={0}
173
- placeholder="Number of history turns"
174
  />
175
  </>
176
 
@@ -179,8 +181,8 @@ export default function QuerySettings() {
179
  <>
180
  <Text
181
  className="ml-1"
182
- text="High-Level Keywords"
183
- tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
184
  side="left"
185
  />
186
  <Input
@@ -194,15 +196,15 @@ export default function QuerySettings() {
194
  .filter((k) => k !== '')
195
  handleChange('hl_keywords', keywords)
196
  }}
197
- placeholder="Enter keywords"
198
  />
199
  </>
200
 
201
  <>
202
  <Text
203
  className="ml-1"
204
- text="Low-Level Keywords"
205
- tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
206
  side="left"
207
  />
208
  <Input
@@ -216,7 +218,7 @@ export default function QuerySettings() {
216
  .filter((k) => k !== '')
217
  handleChange('ll_keywords', keywords)
218
  }}
219
- placeholder="Enter keywords"
220
  />
221
  </>
222
  </>
@@ -226,8 +228,8 @@ export default function QuerySettings() {
226
  <div className="flex items-center gap-2">
227
  <Text
228
  className="ml-1"
229
- text="Only Need Context"
230
- tooltip="If True, only returns the retrieved context without generating a response"
231
  side="left"
232
  />
233
  <div className="grow" />
@@ -242,8 +244,8 @@ export default function QuerySettings() {
242
  <div className="flex items-center gap-2">
243
  <Text
244
  className="ml-1"
245
- text="Only Need Prompt"
246
- tooltip="If True, only returns the generated prompt without producing a response"
247
  side="left"
248
  />
249
  <div className="grow" />
@@ -258,8 +260,8 @@ export default function QuerySettings() {
258
  <div className="flex items-center gap-2">
259
  <Text
260
  className="ml-1"
261
- text="Stream Response"
262
- tooltip="If True, enables streaming output for real-time responses"
263
  side="left"
264
  />
265
  <div className="grow" />
 
14
  SelectValue
15
  } from '@/components/ui/Select'
16
  import { useSettingsStore } from '@/stores/settings'
17
+ import { useTranslation } from 'react-i18next'
18
 
19
  export default function QuerySettings() {
20
+ const { t } = useTranslation()
21
  const querySettings = useSettingsStore((state) => state.querySettings)
22
 
23
  const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
 
27
  return (
28
  <Card className="flex shrink-0 flex-col">
29
  <CardHeader className="px-4 pt-4 pb-2">
30
+ <CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
31
+ <CardDescription>{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
32
  </CardHeader>
33
  <CardContent className="m-0 flex grow flex-col p-0 text-xs">
34
  <div className="relative size-full">
 
37
  <>
38
  <Text
39
  className="ml-1"
40
+ text={t('retrievePanel.querySettings.queryMode')}
41
+ tooltip={t('retrievePanel.querySettings.queryModeTooltip')}
42
  side="left"
43
  />
44
  <Select
 
50
  </SelectTrigger>
51
  <SelectContent>
52
  <SelectGroup>
53
+ <SelectItem value="naive">{t('retrievePanel.querySettings.queryModeOptions.naive')}</SelectItem>
54
+ <SelectItem value="local">{t('retrievePanel.querySettings.queryModeOptions.local')}</SelectItem>
55
+ <SelectItem value="global">{t('retrievePanel.querySettings.queryModeOptions.global')}</SelectItem>
56
+ <SelectItem value="hybrid">{t('retrievePanel.querySettings.queryModeOptions.hybrid')}</SelectItem>
57
+ <SelectItem value="mix">{t('retrievePanel.querySettings.queryModeOptions.mix')}</SelectItem>
58
  </SelectGroup>
59
  </SelectContent>
60
  </Select>
 
64
  <>
65
  <Text
66
  className="ml-1"
67
+ text={t('retrievePanel.querySettings.responseFormat')}
68
+ tooltip={t('retrievePanel.querySettings.responseFormatTooltip')}
69
  side="left"
70
  />
71
  <Select
 
77
  </SelectTrigger>
78
  <SelectContent>
79
  <SelectGroup>
80
+ <SelectItem value="Multiple Paragraphs">{t('retrievePanel.querySettings.responseFormatOptions.multipleParagraphs')}</SelectItem>
81
+ <SelectItem value="Single Paragraph">{t('retrievePanel.querySettings.responseFormatOptions.singleParagraph')}</SelectItem>
82
+ <SelectItem value="Bullet Points">{t('retrievePanel.querySettings.responseFormatOptions.bulletPoints')}</SelectItem>
83
  </SelectGroup>
84
  </SelectContent>
85
  </Select>
 
89
  <>
90
  <Text
91
  className="ml-1"
92
+ text={t('retrievePanel.querySettings.topK')}
93
+ tooltip={t('retrievePanel.querySettings.topKTooltip')}
94
  side="left"
95
  />
96
  <NumberInput
 
99
  value={querySettings.top_k}
100
  onValueChange={(v) => handleChange('top_k', v)}
101
  min={1}
102
+ placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
103
  />
104
  </>
105
 
 
108
  <>
109
  <Text
110
  className="ml-1"
111
+ text={t('retrievePanel.querySettings.maxTokensTextUnit')}
112
+ tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
113
  side="left"
114
  />
115
  <NumberInput
 
118
  value={querySettings.max_token_for_text_unit}
119
  onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
120
  min={1}
121
+ placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
122
  />
123
  </>
124
 
125
  <>
126
  <Text
127
+ text={t('retrievePanel.querySettings.maxTokensGlobalContext')}
128
+ tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
129
  side="left"
130
  />
131
  <NumberInput
 
134
  value={querySettings.max_token_for_global_context}
135
  onValueChange={(v) => handleChange('max_token_for_global_context', v)}
136
  min={1}
137
+ placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
138
  />
139
  </>
140
 
141
  <>
142
  <Text
143
  className="ml-1"
144
+ text={t('retrievePanel.querySettings.maxTokensLocalContext')}
145
+ tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
146
  side="left"
147
  />
148
  <NumberInput
 
151
  value={querySettings.max_token_for_local_context}
152
  onValueChange={(v) => handleChange('max_token_for_local_context', v)}
153
  min={1}
154
+ placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
155
  />
156
  </>
157
  </>
 
160
  <>
161
  <Text
162
  className="ml-1"
163
+ text={t('retrievePanel.querySettings.historyTurns')}
164
+ tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
165
  side="left"
166
  />
167
  <NumberInput
 
172
  value={querySettings.history_turns}
173
  onValueChange={(v) => handleChange('history_turns', v)}
174
  min={0}
175
+ placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
176
  />
177
  </>
178
 
 
181
  <>
182
  <Text
183
  className="ml-1"
184
+ text={t('retrievePanel.querySettings.hlKeywords')}
185
+ tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
186
  side="left"
187
  />
188
  <Input
 
196
  .filter((k) => k !== '')
197
  handleChange('hl_keywords', keywords)
198
  }}
199
+ placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
200
  />
201
  </>
202
 
203
  <>
204
  <Text
205
  className="ml-1"
206
+ text={t('retrievePanel.querySettings.llKeywords')}
207
+ tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
208
  side="left"
209
  />
210
  <Input
 
218
  .filter((k) => k !== '')
219
  handleChange('ll_keywords', keywords)
220
  }}
221
+ placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
222
  />
223
  </>
224
  </>
 
228
  <div className="flex items-center gap-2">
229
  <Text
230
  className="ml-1"
231
+ text={t('retrievePanel.querySettings.onlyNeedContext')}
232
+ tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
233
  side="left"
234
  />
235
  <div className="grow" />
 
244
  <div className="flex items-center gap-2">
245
  <Text
246
  className="ml-1"
247
+ text={t('retrievePanel.querySettings.onlyNeedPrompt')}
248
+ tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
249
  side="left"
250
  />
251
  <div className="grow" />
 
260
  <div className="flex items-center gap-2">
261
  <Text
262
  className="ml-1"
263
+ text={t('retrievePanel.querySettings.streamResponse')}
264
+ tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
265
  side="left"
266
  />
267
  <div className="grow" />
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import { useState, useEffect, useCallback } from 'react'
 
2
  import Button from '@/components/ui/Button'
3
  import {
4
  Table,
@@ -22,6 +23,7 @@ import { useBackendState } from '@/stores/state'
22
  import { RefreshCwIcon } from 'lucide-react'
23
 
24
  export default function DocumentManager() {
 
25
  const health = useBackendState.use.health()
26
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
27
 
@@ -44,7 +46,7 @@ export default function DocumentManager() {
44
  setDocs(null)
45
  }
46
  } catch (err) {
47
- toast.error('Failed to load documents\n' + errorMessage(err))
48
  }
49
  }, [setDocs])
50
 
@@ -57,7 +59,7 @@ export default function DocumentManager() {
57
  const { status } = await scanNewDocuments()
58
  toast.message(status)
59
  } catch (err) {
60
- toast.error('Failed to load documents\n' + errorMessage(err))
61
  }
62
  }, [])
63
 
@@ -69,7 +71,7 @@ export default function DocumentManager() {
69
  try {
70
  await fetchDocuments()
71
  } catch (err) {
72
- toast.error('Failed to get scan progress\n' + errorMessage(err))
73
  }
74
  }, 5000)
75
  return () => clearInterval(interval)
@@ -78,7 +80,7 @@ export default function DocumentManager() {
78
  return (
79
  <Card className="!size-full !rounded-none !border-none">
80
  <CardHeader>
81
- <CardTitle className="text-lg">Document Management</CardTitle>
82
  </CardHeader>
83
  <CardContent className="space-y-4">
84
  <div className="flex gap-2">
@@ -86,10 +88,10 @@ export default function DocumentManager() {
86
  variant="outline"
87
  onClick={scanDocuments}
88
  side="bottom"
89
- tooltip="Scan documents"
90
  size="sm"
91
  >
92
- <RefreshCwIcon /> Scan
93
  </Button>
94
  <div className="flex-1" />
95
  <ClearDocumentsDialog />
@@ -98,29 +100,29 @@ export default function DocumentManager() {
98
 
99
  <Card>
100
  <CardHeader>
101
- <CardTitle>Uploaded documents</CardTitle>
102
- <CardDescription>view the uploaded documents here</CardDescription>
103
  </CardHeader>
104
 
105
  <CardContent>
106
  {!docs && (
107
  <EmptyCard
108
- title="No documents uploaded"
109
- description="upload documents to see them here"
110
  />
111
  )}
112
  {docs && (
113
  <Table>
114
  <TableHeader>
115
  <TableRow>
116
- <TableHead>ID</TableHead>
117
- <TableHead>Summary</TableHead>
118
- <TableHead>Status</TableHead>
119
- <TableHead>Length</TableHead>
120
- <TableHead>Chunks</TableHead>
121
- <TableHead>Created</TableHead>
122
- <TableHead>Updated</TableHead>
123
- <TableHead>Metadata</TableHead>
124
  </TableRow>
125
  </TableHeader>
126
  <TableBody className="text-sm">
@@ -137,13 +139,13 @@ export default function DocumentManager() {
137
  </TableCell>
138
  <TableCell>
139
  {status === 'processed' && (
140
- <span className="text-green-600">Completed</span>
141
  )}
142
  {status === 'processing' && (
143
- <span className="text-blue-600">Processing</span>
144
  )}
145
- {status === 'pending' && <span className="text-yellow-600">Pending</span>}
146
- {status === 'failed' && <span className="text-red-600">Failed</span>}
147
  {doc.error && (
148
  <span className="ml-2 text-red-500" title={doc.error}>
149
  ⚠️
 
1
  import { useState, useEffect, useCallback } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
  import Button from '@/components/ui/Button'
4
  import {
5
  Table,
 
23
  import { RefreshCwIcon } from 'lucide-react'
24
 
25
  export default function DocumentManager() {
26
+ const { t } = useTranslation()
27
  const health = useBackendState.use.health()
28
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
29
 
 
46
  setDocs(null)
47
  }
48
  } catch (err) {
49
+ toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) }))
50
  }
51
  }, [setDocs])
52
 
 
59
  const { status } = await scanNewDocuments()
60
  toast.message(status)
61
  } catch (err) {
62
+ toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) }))
63
  }
64
  }, [])
65
 
 
71
  try {
72
  await fetchDocuments()
73
  } catch (err) {
74
+ toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
75
  }
76
  }, 5000)
77
  return () => clearInterval(interval)
 
80
  return (
81
  <Card className="!size-full !rounded-none !border-none">
82
  <CardHeader>
83
+ <CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
84
  </CardHeader>
85
  <CardContent className="space-y-4">
86
  <div className="flex gap-2">
 
88
  variant="outline"
89
  onClick={scanDocuments}
90
  side="bottom"
91
+ tooltip={t('documentPanel.documentManager.scanTooltip')}
92
  size="sm"
93
  >
94
+ <RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
95
  </Button>
96
  <div className="flex-1" />
97
  <ClearDocumentsDialog />
 
100
 
101
  <Card>
102
  <CardHeader>
103
+ <CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
104
+ <CardDescription>{t('documentPanel.documentManager.uploadedDescription')}</CardDescription>
105
  </CardHeader>
106
 
107
  <CardContent>
108
  {!docs && (
109
  <EmptyCard
110
+ title={t('documentPanel.documentManager.emptyTitle')}
111
+ description={t('documentPanel.documentManager.emptyDescription')}
112
  />
113
  )}
114
  {docs && (
115
  <Table>
116
  <TableHeader>
117
  <TableRow>
118
+ <TableHead>{t('documentPanel.documentManager.columns.id')}</TableHead>
119
+ <TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
120
+ <TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
121
+ <TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
122
+ <TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
123
+ <TableHead>{t('documentPanel.documentManager.columns.created')}</TableHead>
124
+ <TableHead>{t('documentPanel.documentManager.columns.updated')}</TableHead>
125
+ <TableHead>{t('documentPanel.documentManager.columns.metadata')}</TableHead>
126
  </TableRow>
127
  </TableHeader>
128
  <TableBody className="text-sm">
 
139
  </TableCell>
140
  <TableCell>
141
  {status === 'processed' && (
142
+ <span className="text-green-600">{t('documentPanel.documentManager.status.completed')}</span>
143
  )}
144
  {status === 'processing' && (
145
+ <span className="text-blue-600">{t('documentPanel.documentManager.status.processing')}</span>
146
  )}
147
+ {status === 'pending' && <span className="text-yellow-600">{t('documentPanel.documentManager.status.pending')}</span>}
148
+ {status === 'failed' && <span className="text-red-600">{t('documentPanel.documentManager.status.failed')}</span>}
149
  {doc.error && (
150
  <span className="ml-2 text-red-500" title={doc.error}>
151
  ⚠️
lightrag_webui/src/features/RetrievalTesting.tsx CHANGED
@@ -8,8 +8,10 @@ import { useDebounce } from '@/hooks/useDebounce'
8
  import QuerySettings from '@/components/retrieval/QuerySettings'
9
  import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
10
  import { EraserIcon, SendIcon } from 'lucide-react'
 
11
 
12
  export default function RetrievalTesting() {
 
13
  const [messages, setMessages] = useState<MessageWithError[]>(
14
  () => useSettingsStore.getState().retrievalHistory || []
15
  )
@@ -89,7 +91,7 @@ export default function RetrievalTesting() {
89
  }
90
  } catch (err) {
91
  // Handle error
92
- updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`, true)
93
  } finally {
94
  // Clear loading and add messages to state
95
  setIsLoading(false)
@@ -98,7 +100,7 @@ export default function RetrievalTesting() {
98
  .setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
99
  }
100
  },
101
- [inputValue, isLoading, messages, setMessages]
102
  )
103
 
104
  const debouncedMessages = useDebounce(messages, 100)
@@ -117,7 +119,7 @@ export default function RetrievalTesting() {
117
  <div className="flex min-h-0 flex-1 flex-col gap-2">
118
  {messages.length === 0 ? (
119
  <div className="text-muted-foreground flex h-full items-center justify-center text-lg">
120
- Start a retrieval by typing your query below
121
  </div>
122
  ) : (
123
  messages.map((message, idx) => (
@@ -143,18 +145,18 @@ export default function RetrievalTesting() {
143
  size="sm"
144
  >
145
  <EraserIcon />
146
- Clear
147
  </Button>
148
  <Input
149
  className="flex-1"
150
  value={inputValue}
151
  onChange={(e) => setInputValue(e.target.value)}
152
- placeholder="Type your query..."
153
  disabled={isLoading}
154
  />
155
  <Button type="submit" variant="default" disabled={isLoading} size="sm">
156
  <SendIcon />
157
- Send
158
  </Button>
159
  </form>
160
  </div>
 
8
  import QuerySettings from '@/components/retrieval/QuerySettings'
9
  import { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
10
  import { EraserIcon, SendIcon } from 'lucide-react'
11
+ import { useTranslation } from 'react-i18next'
12
 
13
  export default function RetrievalTesting() {
14
+ const { t } = useTranslation()
15
  const [messages, setMessages] = useState<MessageWithError[]>(
16
  () => useSettingsStore.getState().retrievalHistory || []
17
  )
 
91
  }
92
  } catch (err) {
93
  // Handle error
94
+ updateAssistantMessage(`${t('retrievePanel.retrieval.error')}\n${errorMessage(err)}`, true)
95
  } finally {
96
  // Clear loading and add messages to state
97
  setIsLoading(false)
 
100
  .setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
101
  }
102
  },
103
+ [inputValue, isLoading, messages, setMessages, t]
104
  )
105
 
106
  const debouncedMessages = useDebounce(messages, 100)
 
119
  <div className="flex min-h-0 flex-1 flex-col gap-2">
120
  {messages.length === 0 ? (
121
  <div className="text-muted-foreground flex h-full items-center justify-center text-lg">
122
+ {t('retrievePanel.retrieval.startPrompt')}
123
  </div>
124
  ) : (
125
  messages.map((message, idx) => (
 
145
  size="sm"
146
  >
147
  <EraserIcon />
148
+ {t('retrievePanel.retrieval.clear')}
149
  </Button>
150
  <Input
151
  className="flex-1"
152
  value={inputValue}
153
  onChange={(e) => setInputValue(e.target.value)}
154
+ placeholder={t('retrievePanel.retrieval.placeholder')}
155
  disabled={isLoading}
156
  />
157
  <Button type="submit" variant="default" disabled={isLoading} size="sm">
158
  <SendIcon />
159
+ {t('retrievePanel.retrieval.send')}
160
  </Button>
161
  </form>
162
  </div>
lightrag_webui/src/features/SiteHeader.tsx CHANGED
@@ -4,6 +4,7 @@ import ThemeToggle from '@/components/ThemeToggle'
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
  import { useSettingsStore } from '@/stores/settings'
6
  import { cn } from '@/lib/utils'
 
7
 
8
  import { ZapIcon, GithubIcon } from 'lucide-react'
9
 
@@ -29,21 +30,22 @@ function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
29
 
30
  function TabsNavigation() {
31
  const currentTab = useSettingsStore.use.currentTab()
 
32
 
33
  return (
34
  <div className="flex h-8 self-center">
35
  <TabsList className="h-full gap-2">
36
  <NavigationTab value="documents" currentTab={currentTab}>
37
- Documents
38
  </NavigationTab>
39
  <NavigationTab value="knowledge-graph" currentTab={currentTab}>
40
- Knowledge Graph
41
  </NavigationTab>
42
  <NavigationTab value="retrieval" currentTab={currentTab}>
43
- Retrieval
44
  </NavigationTab>
45
  <NavigationTab value="api" currentTab={currentTab}>
46
- API
47
  </NavigationTab>
48
  </TabsList>
49
  </div>
@@ -51,6 +53,7 @@ function TabsNavigation() {
51
  }
52
 
53
  export default function SiteHeader() {
 
54
  return (
55
  <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">
56
  <a href="/" className="mr-6 flex items-center gap-2">
@@ -64,7 +67,7 @@ export default function SiteHeader() {
64
  </div>
65
 
66
  <nav className="flex items-center">
67
- <Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
68
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
69
  <GithubIcon className="size-4" aria-hidden="true" />
70
  </a>
 
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
  import { useSettingsStore } from '@/stores/settings'
6
  import { cn } from '@/lib/utils'
7
+ import { useTranslation } from 'react-i18next'
8
 
9
  import { ZapIcon, GithubIcon } from 'lucide-react'
10
 
 
30
 
31
  function TabsNavigation() {
32
  const currentTab = useSettingsStore.use.currentTab()
33
+ const { t } = useTranslation()
34
 
35
  return (
36
  <div className="flex h-8 self-center">
37
  <TabsList className="h-full gap-2">
38
  <NavigationTab value="documents" currentTab={currentTab}>
39
+ {t('header.documents')}
40
  </NavigationTab>
41
  <NavigationTab value="knowledge-graph" currentTab={currentTab}>
42
+ {t('header.knowledgeGraph')}
43
  </NavigationTab>
44
  <NavigationTab value="retrieval" currentTab={currentTab}>
45
+ {t('header.retrieval')}
46
  </NavigationTab>
47
  <NavigationTab value="api" currentTab={currentTab}>
48
+ {t('header.api')}
49
  </NavigationTab>
50
  </TabsList>
51
  </div>
 
53
  }
54
 
55
  export default function SiteHeader() {
56
+ const { t } = useTranslation()
57
  return (
58
  <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">
59
  <a href="/" className="mr-6 flex items-center gap-2">
 
67
  </div>
68
 
69
  <nav className="flex items-center">
70
+ <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.projectRepository')}>
71
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
72
  <GithubIcon className="size-4" aria-hidden="true" />
73
  </a>
lightrag_webui/src/i18n.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+
4
+ import en from "./locales/en.json";
5
+ import zh from "./locales/zh.json";
6
+
7
+ i18n
8
+ .use(initReactI18next)
9
+ .init({
10
+ resources: {
11
+ en: { translation: en },
12
+ zh: { translation: zh }
13
+ },
14
+ lng: "en", // default
15
+ fallbackLng: "en",
16
+ interpolation: {
17
+ escapeValue: false
18
+ }
19
+ });
20
+
21
+ export default i18n;
lightrag_webui/src/locales/en.json ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "header": {
3
+ "documents": "Documents",
4
+ "knowledgeGraph": "Knowledge Graph",
5
+ "retrieval": "Retrieval",
6
+ "api": "API",
7
+ "projectRepository": "Project Repository",
8
+ "themeToggle": {
9
+ "switchToLight": "Switch to light theme",
10
+ "switchToDark": "Switch to dark theme"
11
+ }
12
+ },
13
+ "documentPanel": {
14
+ "clearDocuments": {
15
+ "button": "Clear",
16
+ "tooltip": "Clear documents",
17
+ "title": "Clear Documents",
18
+ "confirm": "Do you really want to clear all documents?",
19
+ "confirmButton": "YES",
20
+ "success": "Documents cleared successfully",
21
+ "failed": "Clear Documents Failed:\n{{message}}",
22
+ "error": "Clear Documents Failed:\n{{error}}"
23
+ },
24
+ "uploadDocuments": {
25
+ "button": "Upload",
26
+ "tooltip": "Upload documents",
27
+ "title": "Upload Documents",
28
+ "description": "Drag and drop your documents here or click to browse.",
29
+ "uploading": "Uploading {{name}}: {{percent}}%",
30
+ "success": "Upload Success:\n{{name}} uploaded successfully",
31
+ "failed": "Upload Failed:\n{{name}}\n{{message}}",
32
+ "error": "Upload Failed:\n{{name}}\n{{error}}",
33
+ "generalError": "Upload Failed\n{{error}}",
34
+ "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"
35
+ },
36
+ "documentManager": {
37
+ "title": "Document Management",
38
+ "scanButton": "Scan",
39
+ "scanTooltip": "Scan documents",
40
+ "uploadedTitle": "Uploaded Documents",
41
+ "uploadedDescription": "List of uploaded documents and their statuses.",
42
+ "emptyTitle": "No Documents",
43
+ "emptyDescription": "There are no uploaded documents yet.",
44
+ "columns": {
45
+ "id": "ID",
46
+ "summary": "Summary",
47
+ "status": "Status",
48
+ "length": "Length",
49
+ "chunks": "Chunks",
50
+ "created": "Created",
51
+ "updated": "Updated",
52
+ "metadata": "Metadata"
53
+ },
54
+ "status": {
55
+ "completed": "Completed",
56
+ "processing": "Processing",
57
+ "pending": "Pending",
58
+ "failed": "Failed"
59
+ },
60
+ "errors": {
61
+ "loadFailed": "Failed to load documents\n{{error}}",
62
+ "scanFailed": "Failed to scan documents\n{{error}}",
63
+ "scanProgressFailed": "Failed to get scan progress\n{{error}}"
64
+ }
65
+ }
66
+ },
67
+ "graphPanel": {
68
+ "sideBar": {
69
+ "settings": {
70
+ "settings": "Settings",
71
+ "healthCheck": "Health Check",
72
+ "showPropertyPanel": "Show Property Panel",
73
+ "showSearchBar": "Show Search Bar",
74
+ "showNodeLabel": "Show Node Label",
75
+ "nodeDraggable": "Node Draggable",
76
+ "showEdgeLabel": "Show Edge Label",
77
+ "hideUnselectedEdges": "Hide Unselected Edges",
78
+ "edgeEvents": "Edge Events",
79
+ "maxQueryDepth": "Max Query Depth",
80
+ "minDegree": "Minimum Degree",
81
+ "maxLayoutIterations": "Max Layout Iterations",
82
+ "apiKey": "API Key",
83
+ "enterYourAPIkey": "Enter your API key",
84
+ "save": "Save",
85
+ "refreshLayout": "Refresh Layout"
86
+ },
87
+
88
+ "zoomControl": {
89
+ "zoomIn": "Zoom In",
90
+ "zoomOut": "Zoom Out",
91
+ "resetZoom": "Reset Zoom"
92
+ },
93
+
94
+ "layoutsControl": {
95
+ "startAnimation": "Start the layout animation",
96
+ "stopAnimation": "Stop the layout animation",
97
+ "layoutGraph": "Layout Graph",
98
+ "layouts": {
99
+ "Circular": "Circular",
100
+ "Circlepack": "Circlepack",
101
+ "Random": "Random",
102
+ "Noverlaps": "Noverlaps",
103
+ "Force Directed": "Force Directed",
104
+ "Force Atlas": "Force Atlas"
105
+ }
106
+ },
107
+
108
+ "fullScreenControl": {
109
+ "fullScreen": "Full Screen",
110
+ "windowed": "Windowed"
111
+ }
112
+ },
113
+ "statusIndicator": {
114
+ "connected": "Connected",
115
+ "disconnected": "Disconnected"
116
+ },
117
+ "statusCard": {
118
+ "unavailable": "Status information unavailable",
119
+ "storageInfo": "Storage Info",
120
+ "workingDirectory": "Working Directory",
121
+ "inputDirectory": "Input Directory",
122
+ "llmConfig": "LLM Configuration",
123
+ "llmBinding": "LLM Binding",
124
+ "llmBindingHost": "LLM Binding Host",
125
+ "llmModel": "LLM Model",
126
+ "maxTokens": "Max Tokens",
127
+ "embeddingConfig": "Embedding Configuration",
128
+ "embeddingBinding": "Embedding Binding",
129
+ "embeddingBindingHost": "Embedding Binding Host",
130
+ "embeddingModel": "Embedding Model",
131
+ "storageConfig": "Storage Configuration",
132
+ "kvStorage": "KV Storage",
133
+ "docStatusStorage": "Doc Status Storage",
134
+ "graphStorage": "Graph Storage",
135
+ "vectorStorage": "Vector Storage"
136
+ },
137
+ "propertiesView": {
138
+ "node": {
139
+ "title": "Node",
140
+ "id": "ID",
141
+ "labels": "Labels",
142
+ "degree": "Degree",
143
+ "properties": "Properties",
144
+ "relationships": "Relationships"
145
+ },
146
+ "edge": {
147
+ "title": "Relationship",
148
+ "id": "ID",
149
+ "type": "Type",
150
+ "source": "Source",
151
+ "target": "Target",
152
+ "properties": "Properties"
153
+ }
154
+ },
155
+ "search": {
156
+ "placeholder": "Search nodes...",
157
+ "message": "And {count} others"
158
+ },
159
+ "graphLabels": {
160
+ "selectTooltip": "Select query label",
161
+ "noLabels": "No labels found",
162
+ "label": "Label",
163
+ "placeholder": "Search labels...",
164
+ "andOthers": "And {count} others"
165
+ }
166
+ },
167
+ "retrievePanel": {
168
+ "chatMessage": {
169
+ "copyTooltip": "Copy to clipboard",
170
+ "copyError": "Failed to copy text to clipboard"
171
+ },
172
+ "retrieval": {
173
+ "startPrompt": "Start a retrieval by typing your query below",
174
+ "clear": "Clear",
175
+ "send": "Send",
176
+ "placeholder": "Type your query...",
177
+ "error": "Error: Failed to get response"
178
+ },
179
+ "querySettings": {
180
+ "parametersTitle": "Parameters",
181
+ "parametersDescription": "Configure your query parameters",
182
+
183
+ "queryMode": "Query Mode",
184
+ "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",
185
+ "queryModeOptions": {
186
+ "naive": "Naive",
187
+ "local": "Local",
188
+ "global": "Global",
189
+ "hybrid": "Hybrid",
190
+ "mix": "Mix"
191
+ },
192
+
193
+ "responseFormat": "Response Format",
194
+ "responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points",
195
+ "responseFormatOptions": {
196
+ "multipleParagraphs": "Multiple Paragraphs",
197
+ "singleParagraph": "Single Paragraph",
198
+ "bulletPoints": "Bullet Points"
199
+ },
200
+
201
+ "topK": "Top K Results",
202
+ "topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode",
203
+ "topKPlaceholder": "Number of results",
204
+
205
+ "maxTokensTextUnit": "Max Tokens for Text Unit",
206
+ "maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk",
207
+
208
+ "maxTokensGlobalContext": "Max Tokens for Global Context",
209
+ "maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval",
210
+
211
+ "maxTokensLocalContext": "Max Tokens for Local Context",
212
+ "maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval",
213
+
214
+ "historyTurns": "History Turns",
215
+ "historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context",
216
+ "historyTurnsPlaceholder": "Number of history turns",
217
+
218
+ "hlKeywords": "High-Level Keywords",
219
+ "hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas",
220
+ "hlkeywordsPlaceHolder": "Enter keywords",
221
+
222
+ "llKeywords": "Low-Level Keywords",
223
+ "llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas",
224
+
225
+ "onlyNeedContext": "Only Need Context",
226
+ "onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response",
227
+
228
+ "onlyNeedPrompt": "Only Need Prompt",
229
+ "onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response",
230
+
231
+ "streamResponse": "Stream Response",
232
+ "streamResponseTooltip": "If True, enables streaming output for real-time responses"
233
+ }
234
+ }
235
+ }
lightrag_webui/src/locales/zh.json ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "header": {
3
+ "documents": "文档",
4
+ "knowledgeGraph": "知识图谱",
5
+ "retrieval": "检索",
6
+ "api": "API",
7
+ "projectRepository": "项目仓库",
8
+ "themeToggle": {
9
+ "switchToLight": "切换到亮色主题",
10
+ "switchToDark": "切换到暗色主题"
11
+ }
12
+ },
13
+ "documentPanel": {
14
+ "clearDocuments": {
15
+ "button": "清除",
16
+ "tooltip": "清除文档",
17
+ "title": "清除文档",
18
+ "confirm": "您确定要清除所有文档吗?",
19
+ "confirmButton": "确定",
20
+ "success": "文档已成功清除",
21
+ "failed": "清除文档失败:\n{{message}}",
22
+ "error": "清除文档失败:\n{{error}}"
23
+ },
24
+ "uploadDocuments": {
25
+ "button": "上传",
26
+ "tooltip": "上传文档",
27
+ "title": "上传文档",
28
+ "description": "拖放文档到此处或点击浏览。",
29
+ "uploading": "正在上传 {{name}}: {{percent}}%",
30
+ "success": "上传成功:\n{{name}} 上传成功",
31
+ "failed": "上传失败:\n{{name}}\n{{message}}",
32
+ "error": "上传失败:\n{{name}}\n{{error}}",
33
+ "generalError": "上传失败\n{{error}}",
34
+ "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"
35
+ },
36
+ "documentManager": {
37
+ "title": "文档管理",
38
+ "scanButton": "扫描",
39
+ "scanTooltip": "扫描文档",
40
+ "uploadedTitle": "已上传文档",
41
+ "uploadedDescription": "已上传文档及其状态列表。",
42
+ "emptyTitle": "暂无文档",
43
+ "emptyDescription": "尚未上传任何文档。",
44
+ "columns": {
45
+ "id": "ID",
46
+ "summary": "摘要",
47
+ "status": "状态",
48
+ "length": "长度",
49
+ "chunks": "分块",
50
+ "created": "创建时间",
51
+ "updated": "更新时间",
52
+ "metadata": "元数据"
53
+ },
54
+ "status": {
55
+ "completed": "已完成",
56
+ "processing": "处理中",
57
+ "pending": "待处理",
58
+ "failed": "失败"
59
+ },
60
+ "errors": {
61
+ "loadFailed": "加载文档失败\n{{error}}",
62
+ "scanFailed": "扫描文档失败\n{{error}}",
63
+ "scanProgressFailed": "获取扫描进度失败\n{{error}}"
64
+ }
65
+ }
66
+ },
67
+ "graphPanel": {
68
+ "sideBar": {
69
+ "settings": {
70
+ "settings": "设置",
71
+ "healthCheck": "健康检查",
72
+ "showPropertyPanel": "显示属性面板",
73
+ "showSearchBar": "显示搜索栏",
74
+ "showNodeLabel": "显示节点标签",
75
+ "nodeDraggable": "节点可拖动",
76
+ "showEdgeLabel": "显示边标签",
77
+ "hideUnselectedEdges": "隐藏未选中边",
78
+ "edgeEvents": "边事件",
79
+ "maxQueryDepth": "最大查询深度",
80
+ "minDegree": "最小度数",
81
+ "maxLayoutIterations": "最大布局迭代次数",
82
+ "apiKey": "API 密钥",
83
+ "enterYourAPIkey": "输入您的 API 密钥",
84
+ "save": "保存",
85
+ "refreshLayout": "刷新布局"
86
+ },
87
+
88
+ "zoomControl": {
89
+ "zoomIn": "放大",
90
+ "zoomOut": "缩小",
91
+ "resetZoom": "重置缩放"
92
+ },
93
+
94
+ "layoutsControl": {
95
+ "startAnimation": "开始布局动画",
96
+ "stopAnimation": "停止布局动画",
97
+ "layoutGraph": "布局图",
98
+ "layouts": {
99
+ "Circular": "环形布局",
100
+ "Circlepack": "圆形打包布局",
101
+ "Random": "随机布局",
102
+ "Noverlaps": "无重叠布局",
103
+ "Force Directed": "力导向布局",
104
+ "Force Atlas": "力导向图谱布局"
105
+ }
106
+ },
107
+
108
+ "fullScreenControl": {
109
+ "fullScreen": "全屏",
110
+ "windowed": "窗口模式"
111
+ }
112
+ },
113
+ "statusIndicator": {
114
+ "connected": "已连接",
115
+ "disconnected": "未连接"
116
+ },
117
+ "statusCard": {
118
+ "unavailable": "状态信息不可用",
119
+ "storageInfo": "存储信息",
120
+ "workingDirectory": "工作目录",
121
+ "inputDirectory": "输入目录",
122
+ "llmConfig": "LLM 配置",
123
+ "llmBinding": "LLM 绑定",
124
+ "llmBindingHost": "LLM 绑定主机",
125
+ "llmModel": "LLM 模型",
126
+ "maxTokens": "最大 Token 数",
127
+ "embeddingConfig": "嵌入配置",
128
+ "embeddingBinding": "嵌入绑定",
129
+ "embeddingBindingHost": "嵌入绑定主机",
130
+ "embeddingModel": "嵌入模型",
131
+ "storageConfig": "存储配置",
132
+ "kvStorage": "KV 存储",
133
+ "docStatusStorage": "文档状态存储",
134
+ "graphStorage": "图存储",
135
+ "vectorStorage": "向量存储"
136
+ },
137
+ "propertiesView": {
138
+ "node": {
139
+ "title": "节点",
140
+ "id": "ID",
141
+ "labels": "标签",
142
+ "degree": "度数",
143
+ "properties": "属性",
144
+ "relationships": "关系"
145
+ },
146
+ "edge": {
147
+ "title": "关系",
148
+ "id": "ID",
149
+ "type": "类型",
150
+ "source": "源",
151
+ "target": "��标",
152
+ "properties": "属性"
153
+ }
154
+ },
155
+ "search": {
156
+ "placeholder": "搜索节点...",
157
+ "message": "以及其它 {count} 项"
158
+ },
159
+ "graphLabels": {
160
+ "selectTooltip": "选择查询标签",
161
+ "noLabels": "未找到标签",
162
+ "label": "标签",
163
+ "placeholder": "搜索标签...",
164
+ "andOthers": "以及其它 {count} 个"
165
+ }
166
+ },
167
+ "retrievePanel": {
168
+ "chatMessage": {
169
+ "copyTooltip": "复制到剪贴板",
170
+ "copyError": "无法复制文本到剪贴板"
171
+ },
172
+
173
+ "retrieval": {
174
+ "startPrompt": "在下面输入您的查询以开始检索",
175
+ "clear": "清除",
176
+ "send": "发送",
177
+ "placeholder": "输入您的查询...",
178
+ "error": "错误:无法获取响应"
179
+ },
180
+ "querySettings": {
181
+ "parametersTitle": "参数设置",
182
+ "parametersDescription": "配置查询参数",
183
+
184
+ "queryMode": "查询模式",
185
+ "queryModeTooltip": "选择检索策略:\n• 朴素:不使用高级技术的基本搜索\n• 本地:基于上下文的信息检索\n• 全局:利用全局知识库\n• 混合:结合本地和全局检索\n• 综合:集成知识图谱与向量检索",
186
+ "queryModeOptions": {
187
+ "naive": "朴素",
188
+ "local": "本地",
189
+ "global": "全局",
190
+ "hybrid": "混合",
191
+ "mix": "综合"
192
+ },
193
+
194
+ "responseFormat": "响应格式",
195
+ "responseFormatTooltip": "定义响应格式。例如:\n• 多个段落\n• 单个段落\n• 项目符号",
196
+ "responseFormatOptions": {
197
+ "multipleParagraphs": "多个段落",
198
+ "singleParagraph": "单个段落",
199
+ "bulletPoints": "项目符号"
200
+ },
201
+
202
+ "topK": "Top K 结果数",
203
+ "topKTooltip": "要检索的前 K 个项目数量。在“本地”模式下表示实体,在“全局”模式下表示关系",
204
+ "topKPlaceholder": "结果数",
205
+
206
+ "maxTokensTextUnit": "文本单元最大 Token 数",
207
+ "maxTokensTextUnitTooltip": "每个检索到的文本块允许的最大 Token 数",
208
+
209
+ "maxTokensGlobalContext": "全局上下文最大 Token 数",
210
+ "maxTokensGlobalContextTooltip": "在全局检索中为关系描述分配的最大 Token 数",
211
+
212
+ "maxTokensLocalContext": "本地上下文最大 Token 数",
213
+ "maxTokensLocalContextTooltip": "在本地检索中为实体描述分配的最大 Token 数",
214
+
215
+ "historyTurns": "历史轮次",
216
+ "historyTurnsTooltip": "在响应上下文中考虑的完整对话轮次(用户-助手对)",
217
+ "historyTurnsPlaceholder": "历史轮次的数量",
218
+
219
+ "hlKeywords": "高级关键词",
220
+ "hlKeywordsTooltip": "检索时优先考虑的高级关键词。请用逗号分隔",
221
+ "hlkeywordsPlaceHolder": "输入关键词",
222
+
223
+ "llKeywords": "低级关键词",
224
+ "llKeywordsTooltip": "用于优化检索焦点的低级关键词。请用逗号分隔",
225
+
226
+ "onlyNeedContext": "仅需要上下文",
227
+ "onlyNeedContextTooltip": "如果为 True,则仅返回检索到的上下文,而不会生成回复",
228
+
229
+ "onlyNeedPrompt": "仅需要提示",
230
+ "onlyNeedPromptTooltip": "如果为 True,则仅返回生成的提示,而不会生成回复",
231
+
232
+ "streamResponse": "流式响应",
233
+ "streamResponseTooltip": "如果为 True,则启用流式输出以获得实时响应"
234
+ }
235
+ }
236
+ }
lightrag_webui/src/main.tsx CHANGED
@@ -2,6 +2,8 @@ import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
  import App from './App.tsx'
 
 
5
 
6
  createRoot(document.getElementById('root')!).render(
7
  <StrictMode>
 
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
  import App from './App.tsx'
5
+ import "./i18n";
6
+
7
 
8
  createRoot(document.getElementById('root')!).render(
9
  <StrictMode>