Merge branch 'improve-property-tooltip' into feat-node-expand
This commit is contained in:
@@ -423,12 +423,24 @@ def create_app(args):
|
|||||||
"update_status": update_status,
|
"update_status": update_status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Custom StaticFiles class to prevent caching of HTML files
|
||||||
|
class NoCacheStaticFiles(StaticFiles):
|
||||||
|
async def get_response(self, path: str, scope):
|
||||||
|
response = await super().get_response(path, scope)
|
||||||
|
if path.endswith(".html"):
|
||||||
|
response.headers["Cache-Control"] = (
|
||||||
|
"no-cache, no-store, must-revalidate"
|
||||||
|
)
|
||||||
|
response.headers["Pragma"] = "no-cache"
|
||||||
|
response.headers["Expires"] = "0"
|
||||||
|
return response
|
||||||
|
|
||||||
# Webui mount webui/index.html
|
# Webui mount webui/index.html
|
||||||
static_dir = Path(__file__).parent / "webui"
|
static_dir = Path(__file__).parent / "webui"
|
||||||
static_dir.mkdir(exist_ok=True)
|
static_dir.mkdir(exist_ok=True)
|
||||||
app.mount(
|
app.mount(
|
||||||
"/webui",
|
"/webui",
|
||||||
StaticFiles(directory=static_dir, html=True, check_dir=True),
|
NoCacheStaticFiles(directory=static_dir, html=True, check_dir=True),
|
||||||
name="webui",
|
name="webui",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -99,6 +99,37 @@ class DocsStatusesResponse(BaseModel):
|
|||||||
statuses: Dict[DocStatus, List[DocStatusResponse]] = {}
|
statuses: Dict[DocStatus, List[DocStatusResponse]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineStatusResponse(BaseModel):
|
||||||
|
"""Response model for pipeline status
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
autoscanned: Whether auto-scan has started
|
||||||
|
busy: Whether the pipeline is currently busy
|
||||||
|
job_name: Current job name (e.g., indexing files/indexing texts)
|
||||||
|
job_start: Job start time as ISO format string (optional)
|
||||||
|
docs: Total number of documents to be indexed
|
||||||
|
batchs: Number of batches for processing documents
|
||||||
|
cur_batch: Current processing batch
|
||||||
|
request_pending: Flag for pending request for processing
|
||||||
|
latest_message: Latest message from pipeline processing
|
||||||
|
history_messages: List of history messages
|
||||||
|
"""
|
||||||
|
|
||||||
|
autoscanned: bool = False
|
||||||
|
busy: bool = False
|
||||||
|
job_name: str = "Default Job"
|
||||||
|
job_start: Optional[str] = None
|
||||||
|
docs: int = 0
|
||||||
|
batchs: int = 0
|
||||||
|
cur_batch: int = 0
|
||||||
|
request_pending: bool = False
|
||||||
|
latest_message: str = ""
|
||||||
|
history_messages: Optional[List[str]] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
extra = "allow" # Allow additional fields from the pipeline status
|
||||||
|
|
||||||
|
|
||||||
class DocumentManager:
|
class DocumentManager:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -247,7 +278,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
if not pm.is_installed("docling"): # type: ignore
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
pm.install("docling")
|
pm.install("docling")
|
||||||
from docling.document_converter import DocumentConverter
|
from docling.document_converter import DocumentConverter # type: ignore
|
||||||
|
|
||||||
converter = DocumentConverter()
|
converter = DocumentConverter()
|
||||||
result = converter.convert(file_path)
|
result = converter.convert(file_path)
|
||||||
@@ -266,7 +297,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
if not pm.is_installed("docling"): # type: ignore
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
pm.install("docling")
|
pm.install("docling")
|
||||||
from docling.document_converter import DocumentConverter
|
from docling.document_converter import DocumentConverter # type: ignore
|
||||||
|
|
||||||
converter = DocumentConverter()
|
converter = DocumentConverter()
|
||||||
result = converter.convert(file_path)
|
result = converter.convert(file_path)
|
||||||
@@ -286,7 +317,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
if not pm.is_installed("docling"): # type: ignore
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
pm.install("docling")
|
pm.install("docling")
|
||||||
from docling.document_converter import DocumentConverter
|
from docling.document_converter import DocumentConverter # type: ignore
|
||||||
|
|
||||||
converter = DocumentConverter()
|
converter = DocumentConverter()
|
||||||
result = converter.convert(file_path)
|
result = converter.convert(file_path)
|
||||||
@@ -307,7 +338,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
|||||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||||
if not pm.is_installed("docling"): # type: ignore
|
if not pm.is_installed("docling"): # type: ignore
|
||||||
pm.install("docling")
|
pm.install("docling")
|
||||||
from docling.document_converter import DocumentConverter
|
from docling.document_converter import DocumentConverter # type: ignore
|
||||||
|
|
||||||
converter = DocumentConverter()
|
converter = DocumentConverter()
|
||||||
result = converter.convert(file_path)
|
result = converter.convert(file_path)
|
||||||
@@ -718,17 +749,33 @@ def create_document_routes(
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@router.get("/pipeline_status", dependencies=[Depends(optional_api_key)])
|
@router.get(
|
||||||
async def get_pipeline_status():
|
"/pipeline_status",
|
||||||
|
dependencies=[Depends(optional_api_key)],
|
||||||
|
response_model=PipelineStatusResponse,
|
||||||
|
)
|
||||||
|
async def get_pipeline_status() -> PipelineStatusResponse:
|
||||||
"""
|
"""
|
||||||
Get the current status of the document indexing pipeline.
|
Get the current status of the document indexing pipeline.
|
||||||
|
|
||||||
This endpoint returns information about the current state of the document processing pipeline,
|
This endpoint returns information about the current state of the document processing pipeline,
|
||||||
including whether it's busy, the current job name, when it started, how many documents
|
including the processing status, progress information, and history messages.
|
||||||
are being processed, how many batches there are, and which batch is currently being processed.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the pipeline status information
|
PipelineStatusResponse: A response object containing:
|
||||||
|
- autoscanned (bool): Whether auto-scan has started
|
||||||
|
- busy (bool): Whether the pipeline is currently busy
|
||||||
|
- job_name (str): Current job name (e.g., indexing files/indexing texts)
|
||||||
|
- job_start (str, optional): Job start time as ISO format string
|
||||||
|
- docs (int): Total number of documents to be indexed
|
||||||
|
- batchs (int): Number of batches for processing documents
|
||||||
|
- cur_batch (int): Current processing batch
|
||||||
|
- request_pending (bool): Flag for pending request for processing
|
||||||
|
- latest_message (str): Latest message from pipeline processing
|
||||||
|
- history_messages (List[str], optional): List of history messages
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If an error occurs while retrieving pipeline status (500)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from lightrag.kg.shared_storage import get_namespace_data
|
from lightrag.kg.shared_storage import get_namespace_data
|
||||||
@@ -746,7 +793,7 @@ def create_document_routes(
|
|||||||
if status_dict.get("job_start"):
|
if status_dict.get("job_start"):
|
||||||
status_dict["job_start"] = str(status_dict["job_start"])
|
status_dict["job_start"] = str(status_dict["job_start"])
|
||||||
|
|
||||||
return status_dict
|
return PipelineStatusResponse(**status_dict)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting pipeline status: {str(e)}")
|
logger.error(f"Error getting pipeline status: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -2,10 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="./logo.png" />
|
<link rel="icon" type="image/svg+xml" href="./logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
<script type="module" crossorigin src="./assets/index-B9TRs-Wk.js"></script>
|
<script type="module" crossorigin src="./assets/index-Cs3ZbEon.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DRGuXfZw.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DRGuXfZw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@@ -661,7 +661,7 @@ class Neo4JStorage(BaseGraphStorage):
|
|||||||
WITH collect({node: n}) AS filtered_nodes
|
WITH collect({node: n}) AS filtered_nodes
|
||||||
UNWIND filtered_nodes AS node_info
|
UNWIND filtered_nodes AS node_info
|
||||||
WITH collect(node_info.node) AS kept_nodes, filtered_nodes
|
WITH collect(node_info.node) AS kept_nodes, filtered_nodes
|
||||||
MATCH (a)-[r]-(b)
|
OPTIONAL MATCH (a)-[r]-(b)
|
||||||
WHERE a IN kept_nodes AND b IN kept_nodes
|
WHERE a IN kept_nodes AND b IN kept_nodes
|
||||||
RETURN filtered_nodes AS node_info,
|
RETURN filtered_nodes AS node_info,
|
||||||
collect(DISTINCT r) AS relationships
|
collect(DISTINCT r) AS relationships
|
||||||
@@ -704,7 +704,7 @@ class Neo4JStorage(BaseGraphStorage):
|
|||||||
WITH collect({node: node}) AS filtered_nodes
|
WITH collect({node: node}) AS filtered_nodes
|
||||||
UNWIND filtered_nodes AS node_info
|
UNWIND filtered_nodes AS node_info
|
||||||
WITH collect(node_info.node) AS kept_nodes, filtered_nodes
|
WITH collect(node_info.node) AS kept_nodes, filtered_nodes
|
||||||
MATCH (a)-[r]-(b)
|
OPTIONAL MATCH (a)-[r]-(b)
|
||||||
WHERE a IN kept_nodes AND b IN kept_nodes
|
WHERE a IN kept_nodes AND b IN kept_nodes
|
||||||
RETURN filtered_nodes AS node_info,
|
RETURN filtered_nodes AS node_info,
|
||||||
collect(DISTINCT r) AS relationships
|
collect(DISTINCT r) AS relationships
|
||||||
|
@@ -2,6 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
|
@@ -13,16 +13,25 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
|
|||||||
* When the selected item changes, highlighted the node and center the camera on it.
|
* When the selected item changes, highlighted the node and center the camera on it.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!node) return
|
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
|
||||||
if (move) {
|
if (move) {
|
||||||
|
if (node) {
|
||||||
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||||
gotoNode(node)
|
gotoNode(node)
|
||||||
|
} else {
|
||||||
|
// If no node is selected but move is true, reset to default view
|
||||||
|
sigma.setCustomBBox(null)
|
||||||
|
sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 })
|
||||||
|
}
|
||||||
useGraphStore.getState().setMoveToSelectedNode(false)
|
useGraphStore.getState().setMoveToSelectedNode(false)
|
||||||
|
} else if (node) {
|
||||||
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (node) {
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [node, move, sigma, gotoNode])
|
}, [node, move, sigma, gotoNode])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
||||||
|
import Graph from 'graphology'
|
||||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
||||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
||||||
import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||||
import useTheme from '@/hooks/useTheme'
|
import useTheme from '@/hooks/useTheme'
|
||||||
import * as Constants from '@/lib/constants'
|
import * as Constants from '@/lib/constants'
|
||||||
|
|
||||||
@@ -21,7 +22,6 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
||||||
const { lightrageGraph } = useLightragGraph()
|
|
||||||
const sigma = useSigma<NodeType, EdgeType>()
|
const sigma = useSigma<NodeType, EdgeType>()
|
||||||
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
||||||
const setSettings = useSetSettings<NodeType, EdgeType>()
|
const setSettings = useSetSettings<NodeType, EdgeType>()
|
||||||
@@ -38,17 +38,18 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
const selectedEdge = useGraphStore.use.selectedEdge()
|
const selectedEdge = useGraphStore.use.selectedEdge()
|
||||||
const focusedEdge = useGraphStore.use.focusedEdge()
|
const focusedEdge = useGraphStore.use.focusedEdge()
|
||||||
|
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount or maxIterations changes
|
* When component mount or maxIterations changes
|
||||||
* => load the graph and apply layout
|
* => load the graph and apply layout
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Create & load the graph
|
if (sigmaGraph) {
|
||||||
const graph = lightrageGraph()
|
loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>)
|
||||||
loadGraph(graph)
|
|
||||||
assignLayout()
|
assignLayout()
|
||||||
}, [assignLayout, loadGraph, lightrageGraph, maxIterations])
|
}
|
||||||
|
}, [assignLayout, loadGraph, sigmaGraph, maxIterations])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount
|
* When component mount
|
||||||
|
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
const GraphLabels = () => {
|
const GraphLabels = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const label = useSettingsStore.use.queryLabel()
|
const label = useSettingsStore.use.queryLabel()
|
||||||
const graphLabels = useGraphStore.use.graphLabels()
|
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
|
||||||
|
|
||||||
const getSearchEngine = useCallback(() => {
|
const getSearchEngine = useCallback(() => {
|
||||||
// Create search engine
|
// Create search engine
|
||||||
@@ -26,14 +26,14 @@ const GraphLabels = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Add documents
|
// Add documents
|
||||||
const documents = graphLabels.map((str, index) => ({ id: index, value: str }))
|
const documents = allDatabaseLabels.map((str, index) => ({ id: index, value: str }))
|
||||||
searchEngine.addAll(documents)
|
searchEngine.addAll(documents)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: graphLabels,
|
labels: allDatabaseLabels,
|
||||||
searchEngine
|
searchEngine
|
||||||
}
|
}
|
||||||
}, [graphLabels])
|
}, [allDatabaseLabels])
|
||||||
|
|
||||||
const fetchData = useCallback(
|
const fetchData = useCallback(
|
||||||
async (query?: string): Promise<string[]> => {
|
async (query?: string): Promise<string[]> => {
|
||||||
@@ -47,24 +47,11 @@ const GraphLabels = () => {
|
|||||||
|
|
||||||
return result.length <= labelListLimit
|
return result.length <= labelListLimit
|
||||||
? result
|
? result
|
||||||
: [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
|
: [...result.slice(0, labelListLimit), '...']
|
||||||
},
|
},
|
||||||
[getSearchEngine]
|
[getSearchEngine]
|
||||||
)
|
)
|
||||||
|
|
||||||
const setQueryLabel = useCallback((newLabel: string) => {
|
|
||||||
if (newLabel.startsWith('And ') && newLabel.endsWith(' others')) return
|
|
||||||
|
|
||||||
const currentLabel = useSettingsStore.getState().queryLabel
|
|
||||||
|
|
||||||
// When selecting the same label (except '*'), switch to '*'
|
|
||||||
if (newLabel === currentLabel && newLabel !== '*') {
|
|
||||||
useSettingsStore.getState().setQueryLabel('*')
|
|
||||||
} else {
|
|
||||||
useSettingsStore.getState().setQueryLabel(newLabel)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncSelect<string>
|
<AsyncSelect<string>
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
@@ -78,8 +65,20 @@ const GraphLabels = () => {
|
|||||||
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
||||||
label={t('graphPanel.graphLabels.label')}
|
label={t('graphPanel.graphLabels.label')}
|
||||||
placeholder={t('graphPanel.graphLabels.placeholder')}
|
placeholder={t('graphPanel.graphLabels.placeholder')}
|
||||||
value={label !== null ? label : ''}
|
value={label !== null ? label : '*'}
|
||||||
onChange={setQueryLabel}
|
onChange={(newLabel) => {
|
||||||
|
const currentLabel = useSettingsStore.getState().queryLabel
|
||||||
|
|
||||||
|
if (newLabel === '...') {
|
||||||
|
newLabel = '*'
|
||||||
|
}
|
||||||
|
if (newLabel === currentLabel && newLabel !== '*') {
|
||||||
|
// 选择相同标签时切换到'*'
|
||||||
|
useSettingsStore.getState().setQueryLabel('*')
|
||||||
|
} else {
|
||||||
|
useSettingsStore.getState().setQueryLabel(newLabel)
|
||||||
|
}
|
||||||
|
}}
|
||||||
clearable={false} // Prevent clearing value on reselect
|
clearable={false} // Prevent clearing value on reselect
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@@ -85,8 +85,19 @@ export const GraphSearchInput = ({
|
|||||||
const loadOptions = useCallback(
|
const loadOptions = useCallback(
|
||||||
async (query?: string): Promise<OptionItem[]> => {
|
async (query?: string): Promise<OptionItem[]> => {
|
||||||
if (onFocus) onFocus(null)
|
if (onFocus) onFocus(null)
|
||||||
if (!query || !searchEngine) return []
|
if (!graph || !searchEngine) return []
|
||||||
const result: OptionItem[] = searchEngine.search(query).map((r) => ({
|
|
||||||
|
// If no query, return first searchResultLimit nodes
|
||||||
|
if (!query) {
|
||||||
|
const nodeIds = graph.nodes().slice(0, searchResultLimit)
|
||||||
|
return nodeIds.map(id => ({
|
||||||
|
id,
|
||||||
|
type: 'nodes'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If has query, search nodes
|
||||||
|
const result: OptionItem[] = searchEngine.search(query).map((r: { id: string }) => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
type: 'nodes'
|
type: 'nodes'
|
||||||
}))
|
}))
|
||||||
@@ -103,7 +114,7 @@ export const GraphSearchInput = ({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[searchEngine, onFocus]
|
[graph, searchEngine, onFocus, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<CommandList hidden={!open || debouncedSearchTerm.length === 0}>
|
<CommandList hidden={!open}>
|
||||||
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
||||||
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||||
// import { MiniMap } from '@react-sigma/minimap'
|
// import { MiniMap } from '@react-sigma/minimap'
|
||||||
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
||||||
import { Settings as SigmaSettings } from 'sigma/settings'
|
import { Settings as SigmaSettings } from 'sigma/settings'
|
||||||
@@ -91,8 +91,12 @@ const GraphEvents = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Disable the autoscale at the first down interaction
|
// Disable the autoscale at the first down interaction
|
||||||
mousedown: () => {
|
mousedown: (e) => {
|
||||||
if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox())
|
// Only set custom BBox if it's a drag operation (mouse button is pressed)
|
||||||
|
const mouseEvent = e.original as MouseEvent;
|
||||||
|
if (mouseEvent.buttons !== 0 && !sigma.getCustomBBox()) {
|
||||||
|
sigma.setCustomBBox(sigma.getBBox())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [registerEvents, sigma, draggedNode])
|
}, [registerEvents, sigma, draggedNode])
|
||||||
@@ -102,6 +106,7 @@ const GraphEvents = () => {
|
|||||||
|
|
||||||
const GraphViewer = () => {
|
const GraphViewer = () => {
|
||||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||||
|
const sigmaRef = useRef<any>(null)
|
||||||
|
|
||||||
const selectedNode = useGraphStore.use.selectedNode()
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
@@ -144,7 +149,11 @@ const GraphViewer = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
|
<SigmaContainer
|
||||||
|
settings={sigmaSettings}
|
||||||
|
className="!bg-background !size-full overflow-hidden"
|
||||||
|
ref={sigmaRef}
|
||||||
|
>
|
||||||
<GraphControl />
|
<GraphControl />
|
||||||
|
|
||||||
{enableNodeDrag && <GraphEvents />}
|
{enableNodeDrag && <GraphEvents />}
|
||||||
|
@@ -162,13 +162,37 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useLightrangeGraph = () => {
|
const useLightrangeGraph = () => {
|
||||||
// Use useRef to maintain lastQueryLabel state between renders
|
|
||||||
const lastQueryLabelRef = useRef({ label: '', maxQueryDepth: 0, minDegree: 0 })
|
|
||||||
const queryLabel = useSettingsStore.use.queryLabel()
|
const queryLabel = useSettingsStore.use.queryLabel()
|
||||||
const rawGraph = useGraphStore.use.rawGraph()
|
const rawGraph = useGraphStore.use.rawGraph()
|
||||||
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||||
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
||||||
const minDegree = useSettingsStore.use.graphMinDegree()
|
const minDegree = useSettingsStore.use.graphMinDegree()
|
||||||
|
const isFetching = useGraphStore.use.isFetching()
|
||||||
|
|
||||||
|
// Fetch all database labels on mount
|
||||||
|
useEffect(() => {
|
||||||
|
useGraphStore.getState().fetchAllDatabaseLabels()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Use ref to track fetch status
|
||||||
|
const fetchStatusRef = useRef<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// Track previous parameters to detect actual changes
|
||||||
|
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree });
|
||||||
|
|
||||||
|
// Reset fetch status only when parameters actually change
|
||||||
|
useEffect(() => {
|
||||||
|
const prevParams = prevParamsRef.current;
|
||||||
|
if (prevParams.queryLabel !== queryLabel ||
|
||||||
|
prevParams.maxQueryDepth !== maxQueryDepth ||
|
||||||
|
prevParams.minDegree !== minDegree) {
|
||||||
|
useGraphStore.getState().setIsFetching(false);
|
||||||
|
// Reset fetch status for new parameters
|
||||||
|
fetchStatusRef.current = {};
|
||||||
|
// Update previous parameters
|
||||||
|
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree };
|
||||||
|
}
|
||||||
|
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
||||||
|
|
||||||
const getNode = useCallback(
|
const getNode = useCallback(
|
||||||
(nodeId: string) => {
|
(nodeId: string) => {
|
||||||
@@ -186,17 +210,21 @@ const useLightrangeGraph = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryLabel) {
|
if (queryLabel) {
|
||||||
// Always fetch data for "*" label
|
const fetchKey = `${queryLabel}-${maxQueryDepth}-${minDegree}`;
|
||||||
// For other labels, only fetch when parameters change
|
|
||||||
const shouldUpdate = true;
|
|
||||||
|
|
||||||
if (shouldUpdate) {
|
// Only fetch if we haven't fetched this combination in the current component lifecycle
|
||||||
lastQueryLabelRef.current = {
|
if (!isFetching && !fetchStatusRef.current[fetchKey]) {
|
||||||
label: queryLabel,
|
const state = useGraphStore.getState();
|
||||||
maxQueryDepth,
|
// Clear selection and highlighted nodes before fetching new graph
|
||||||
minDegree
|
state.clearSelection();
|
||||||
|
if (state.sigmaGraph) {
|
||||||
|
state.sigmaGraph.forEachNode((node) => {
|
||||||
|
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.setIsFetching(true);
|
||||||
|
fetchStatusRef.current[fetchKey] = true;
|
||||||
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
|
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
|
||||||
const state = useGraphStore.getState()
|
const state = useGraphStore.getState()
|
||||||
const newSigmaGraph = createSigmaGraph(data)
|
const newSigmaGraph = createSigmaGraph(data)
|
||||||
@@ -208,7 +236,7 @@ const useLightrangeGraph = () => {
|
|||||||
state.setSigmaGraph(newSigmaGraph)
|
state.setSigmaGraph(newSigmaGraph)
|
||||||
state.setRawGraph(data)
|
state.setRawGraph(data)
|
||||||
|
|
||||||
// Extract labels from graph data
|
// Extract labels from current graph data
|
||||||
if (data) {
|
if (data) {
|
||||||
const labelSet = new Set<string>();
|
const labelSet = new Set<string>();
|
||||||
for (const node of data.nodes) {
|
for (const node of data.nodes) {
|
||||||
@@ -227,6 +255,21 @@ const useLightrangeGraph = () => {
|
|||||||
// Ensure * is there eventhough there is no graph data
|
// Ensure * is there eventhough there is no graph data
|
||||||
state.setGraphLabels(['*']);
|
state.setGraphLabels(['*']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch all database labels after graph update
|
||||||
|
state.fetchAllDatabaseLabels();
|
||||||
|
if (!data) {
|
||||||
|
// If data is invalid, remove the fetch flag to allow retry
|
||||||
|
delete fetchStatusRef.current[fetchKey];
|
||||||
|
}
|
||||||
|
// Reset fetching state after all updates are complete
|
||||||
|
// Reset camera view by triggering FocusOnNode component
|
||||||
|
state.setMoveToSelectedNode(true);
|
||||||
|
state.setIsFetching(false);
|
||||||
|
}).catch(() => {
|
||||||
|
// Reset fetching state and remove flag in case of error
|
||||||
|
useGraphStore.getState().setIsFetching(false);
|
||||||
|
delete fetchStatusRef.current[fetchKey];
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -234,7 +277,7 @@ const useLightrangeGraph = () => {
|
|||||||
state.reset()
|
state.reset()
|
||||||
state.setSigmaGraph(new DirectedGraph())
|
state.setSigmaGraph(new DirectedGraph())
|
||||||
}
|
}
|
||||||
}, [queryLabel, maxQueryDepth, minDegree])
|
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
||||||
|
|
||||||
const lightrageGraph = useCallback(() => {
|
const lightrageGraph = useCallback(() => {
|
||||||
if (sigmaGraph) {
|
if (sigmaGraph) {
|
||||||
|
@@ -15,8 +15,8 @@ export const edgeColorDarkTheme = '#969696'
|
|||||||
export const edgeColorSelected = '#F57F17'
|
export const edgeColorSelected = '#F57F17'
|
||||||
export const edgeColorHighlighted = '#B2EBF2'
|
export const edgeColorHighlighted = '#B2EBF2'
|
||||||
|
|
||||||
export const searchResultLimit = 20
|
export const searchResultLimit = 50
|
||||||
export const labelListLimit = 40
|
export const labelListLimit = 100
|
||||||
|
|
||||||
export const minNodeSize = 4
|
export const minNodeSize = 4
|
||||||
export const maxNodeSize = 20
|
export const maxNodeSize = 20
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { createSelectors } from '@/lib/utils'
|
import { createSelectors } from '@/lib/utils'
|
||||||
import { DirectedGraph } from 'graphology'
|
import { DirectedGraph } from 'graphology'
|
||||||
|
import { getGraphLabels } from '@/api/lightrag'
|
||||||
|
|
||||||
export type RawNodeType = {
|
export type RawNodeType = {
|
||||||
id: string
|
id: string
|
||||||
@@ -66,8 +67,10 @@ interface GraphState {
|
|||||||
rawGraph: RawGraph | null
|
rawGraph: RawGraph | null
|
||||||
sigmaGraph: DirectedGraph | null
|
sigmaGraph: DirectedGraph | null
|
||||||
graphLabels: string[]
|
graphLabels: string[]
|
||||||
|
allDatabaseLabels: string[]
|
||||||
|
|
||||||
moveToSelectedNode: boolean
|
moveToSelectedNode: boolean
|
||||||
|
isFetching: boolean
|
||||||
|
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
||||||
setFocusedNode: (nodeId: string | null) => void
|
setFocusedNode: (nodeId: string | null) => void
|
||||||
@@ -81,6 +84,9 @@ interface GraphState {
|
|||||||
setRawGraph: (rawGraph: RawGraph | null) => void
|
setRawGraph: (rawGraph: RawGraph | null) => void
|
||||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
||||||
setGraphLabels: (labels: string[]) => void
|
setGraphLabels: (labels: string[]) => void
|
||||||
|
setAllDatabaseLabels: (labels: string[]) => void
|
||||||
|
fetchAllDatabaseLabels: () => Promise<void>
|
||||||
|
setIsFetching: (isFetching: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useGraphStoreBase = create<GraphState>()((set) => ({
|
const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||||
@@ -90,11 +96,14 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
focusedEdge: null,
|
focusedEdge: null,
|
||||||
|
|
||||||
moveToSelectedNode: false,
|
moveToSelectedNode: false,
|
||||||
|
isFetching: false,
|
||||||
|
|
||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
sigmaGraph: null,
|
||||||
graphLabels: ['*'],
|
graphLabels: ['*'],
|
||||||
|
allDatabaseLabels: ['*'],
|
||||||
|
|
||||||
|
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||||
set({ selectedNode: nodeId, moveToSelectedNode }),
|
set({ selectedNode: nodeId, moveToSelectedNode }),
|
||||||
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
||||||
@@ -128,6 +137,18 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
|||||||
|
|
||||||
setGraphLabels: (labels: string[]) => set({ graphLabels: labels }),
|
setGraphLabels: (labels: string[]) => set({ graphLabels: labels }),
|
||||||
|
|
||||||
|
setAllDatabaseLabels: (labels: string[]) => set({ allDatabaseLabels: labels }),
|
||||||
|
|
||||||
|
fetchAllDatabaseLabels: async () => {
|
||||||
|
try {
|
||||||
|
const labels = await getGraphLabels();
|
||||||
|
set({ allDatabaseLabels: ['*', ...labels] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch all database labels:', error);
|
||||||
|
set({ allDatabaseLabels: ['*'] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
|
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@@ -171,7 +171,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 7,
|
version: 8,
|
||||||
migrate: (state: any, version: number) => {
|
migrate: (state: any, version: number) => {
|
||||||
if (version < 2) {
|
if (version < 2) {
|
||||||
state.showEdgeLabel = false
|
state.showEdgeLabel = false
|
||||||
@@ -208,7 +208,13 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
}
|
}
|
||||||
if (version < 7) {
|
if (version < 7) {
|
||||||
state.graphQueryMaxDepth = 3
|
state.graphQueryMaxDepth = 3
|
||||||
state.graphLayoutMaxIterations = 10
|
state.graphLayoutMaxIterations = 15
|
||||||
|
}
|
||||||
|
if (version < 8) {
|
||||||
|
state.enableNodeDrag = true
|
||||||
|
state.enableHideUnselectedEdges = true
|
||||||
|
state.enableEdgeEvents = false
|
||||||
|
state.graphMinDegree = 0
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user