diff --git a/lightrag_webui/src/components/graph/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx index 17e7a4d3..e2ac7aae 100644 --- a/lightrag_webui/src/components/graph/GraphLabels.tsx +++ b/lightrag_webui/src/components/graph/GraphLabels.tsx @@ -1,7 +1,8 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState, useRef } from 'react' import { AsyncSelect } from '@/components/ui/AsyncSelect' import { useSettingsStore } from '@/stores/settings' import { useGraphStore } from '@/stores/graph' +import { useBackendState } from '@/stores/state' import { dropdownDisplayLimit, controlButtonVariant, @@ -22,6 +23,11 @@ const GraphLabels = () => { const [refreshTrigger, setRefreshTrigger] = useState(0) const [selectKey, setSelectKey] = useState(0) + // Pipeline state monitoring + const pipelineBusy = useBackendState.use.pipelineBusy() + const prevPipelineBusy = useRef(undefined) + const shouldRefreshPopularLabelsRef = useRef(false) + // Dynamic tooltip based on current label state const getRefreshTooltip = useCallback(() => { if (isRefreshing) { @@ -67,6 +73,49 @@ const GraphLabels = () => { } }, [dropdownRefreshTrigger]) + // Monitor pipeline state changes: busy -> idle + useEffect(() => { + if (prevPipelineBusy.current === true && pipelineBusy === false) { + console.log('Pipeline changed from busy to idle, marking for popular labels refresh') + shouldRefreshPopularLabelsRef.current = true + } + prevPipelineBusy.current = pipelineBusy + }, [pipelineBusy]) + + // Helper: Reload popular labels from backend + const reloadPopularLabels = useCallback(async () => { + if (!shouldRefreshPopularLabelsRef.current) return + + console.log('Reloading popular labels (triggered by pipeline idle)') + try { + const popularLabels = await getPopularLabels(popularLabelsDefaultLimit) + SearchHistoryManager.clearHistory() + + if (popularLabels.length === 0) { + const fallbackLabels = ['entity', 'relationship', 'document', 'concept'] + await SearchHistoryManager.initializeWithDefaults(fallbackLabels) + } else { + await SearchHistoryManager.initializeWithDefaults(popularLabels) + } + } catch (error) { + console.error('Failed to reload popular labels:', error) + const fallbackLabels = ['entity', 'relationship', 'document'] + SearchHistoryManager.clearHistory() + await SearchHistoryManager.initializeWithDefaults(fallbackLabels) + } finally { + // Always clear the flag + shouldRefreshPopularLabelsRef.current = false + } + }, []) + + // Helper: Bump dropdown data to trigger refresh + const bumpDropdownData = useCallback(({ forceSelectKey = false } = {}) => { + setRefreshTrigger(prev => prev + 1) + if (forceSelectKey) { + setSelectKey(prev => prev + 1) + } + }, []) + const fetchData = useCallback( async (query?: string): Promise => { let results: string[] = []; @@ -115,6 +164,12 @@ const GraphLabels = () => { currentLabel = '*' } + // Scenario 1: Manual refresh - reload popular labels if flag is set (regardless of current label) + if (shouldRefreshPopularLabelsRef.current) { + await reloadPopularLabels() + bumpDropdownData({ forceSelectKey: true }) + } + if (currentLabel && currentLabel !== '*') { // Scenario 1: Has specific label, try to refresh current label console.log(`Refreshing current label: ${currentLabel}`) @@ -135,7 +190,7 @@ const GraphLabels = () => { console.log('Refreshing global data and popular labels') try { - // Re-fetch popular labels and update search history + // Re-fetch popular labels and update search history (if not already done) const popularLabels = await getPopularLabels(popularLabelsDefaultLimit) SearchHistoryManager.clearHistory() @@ -173,7 +228,16 @@ const GraphLabels = () => { } finally { setIsRefreshing(false) } - }, [label]) + }, [label, reloadPopularLabels, bumpDropdownData]) + + // Handle dropdown before open - reload popular labels if needed + const handleDropdownBeforeOpen = useCallback(async () => { + const currentLabel = useSettingsStore.getState().queryLabel + if (shouldRefreshPopularLabelsRef.current && (!currentLabel || currentLabel === '*')) { + await reloadPopularLabels() + bumpDropdownData() + } + }, [reloadPopularLabels, bumpDropdownData]) return (
@@ -196,6 +260,7 @@ const GraphLabels = () => { searchInputClassName="max-h-8" triggerTooltip={t('graphPanel.graphLabels.selectTooltip')} fetcher={fetchData} + onBeforeOpen={handleDropdownBeforeOpen} renderOption={(item) => (
{item} diff --git a/lightrag_webui/src/components/ui/AsyncSelect.tsx b/lightrag_webui/src/components/ui/AsyncSelect.tsx index bed0d290..87647cf4 100644 --- a/lightrag_webui/src/components/ui/AsyncSelect.tsx +++ b/lightrag_webui/src/components/ui/AsyncSelect.tsx @@ -43,6 +43,8 @@ export interface AsyncSelectProps { value: string /** Callback when selection changes */ onChange: (value: string) => void + /** Callback before opening the dropdown (async supported) */ + onBeforeOpen?: () => void | Promise /** Accessibility label for the select field */ ariaLabel?: string /** Placeholder text when no selection */ @@ -83,6 +85,7 @@ export function AsyncSelect({ searchPlaceholder, value, onChange, + onBeforeOpen, disabled = false, className, triggerClassName, @@ -196,8 +199,18 @@ export function AsyncSelect({ [selectedValue, onChange, clearable, options, getOptionValue] ) + const handleOpenChange = useCallback( + async (newOpen: boolean) => { + if (newOpen && onBeforeOpen) { + await onBeforeOpen() + } + setOpen(newOpen) + }, + [onBeforeOpen] + ) + return ( - +