From 4cc6388742d3fb60782200335a1eccc8d7740f31 Mon Sep 17 00:00:00 2001 From: yangdx Date: Fri, 31 Oct 2025 04:45:35 +0800 Subject: [PATCH] Add auto-refresh of popular labels when pipeline completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Monitor pipeline busy->idle transitions • Reload labels on dropdown open if needed • Add onBeforeOpen callback to AsyncSelect • Clear refresh flags after processing • Improve label sync with backend state (cherry picked from commit 58c83f9da5c719c40e7ff2ad4ebbd21ad81d5875) --- .../src/components/graph/GraphLabels.tsx | 173 +++++++++++++----- .../src/components/ui/AsyncSelect.tsx | 15 +- 2 files changed, 146 insertions(+), 42 deletions(-) diff --git a/lightrag_webui/src/components/graph/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx index 7ec2b6f5..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, @@ -17,10 +18,16 @@ import { getPopularLabels, searchLabels } from '@/api/lightrag' const GraphLabels = () => { const { t } = useTranslation() const label = useSettingsStore.use.queryLabel() + const dropdownRefreshTrigger = useSettingsStore.use.searchLabelDropdownRefreshTrigger() const [isRefreshing, setIsRefreshing] = useState(false) 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) { @@ -54,6 +61,61 @@ const GraphLabels = () => { initializeHistory() }, []) + // Force AsyncSelect to re-render when label changes externally (e.g., from entity rename/merge) + useEffect(() => { + setSelectKey(prev => prev + 1) + }, [label]) + + // Force AsyncSelect to re-render when dropdown refresh is triggered (e.g., after entity rename) + useEffect(() => { + if (dropdownRefreshTrigger > 0) { + setSelectKey(prev => prev + 1) + } + }, [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[] = []; @@ -102,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}`) @@ -122,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() @@ -160,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 (
@@ -175,49 +252,63 @@ const GraphLabels = () => { > - - key={selectKey} // Force re-render when data changes - className="min-w-[300px]" - triggerClassName="max-h-8" - searchInputClassName="max-h-8" - triggerTooltip={t('graphPanel.graphLabels.selectTooltip')} - fetcher={fetchData} - renderOption={(item) =>
{item}
} - getOptionValue={(item) => item} - getDisplayValue={(item) =>
{item}
} - notFound={
{t('graphPanel.graphLabels.noLabels')}
} - ariaLabel={t('graphPanel.graphLabels.label')} - placeholder={t('graphPanel.graphLabels.placeholder')} - searchPlaceholder={t('graphPanel.graphLabels.placeholder')} - noResultsMessage={t('graphPanel.graphLabels.noLabels')} - value={label !== null ? label : '*'} - onChange={(newLabel) => { - const currentLabel = useSettingsStore.getState().queryLabel; +
+ + key={selectKey} // Force re-render when data changes + className="min-w-[300px]" + triggerClassName="max-h-8 w-full overflow-hidden" + searchInputClassName="max-h-8" + triggerTooltip={t('graphPanel.graphLabels.selectTooltip')} + fetcher={fetchData} + onBeforeOpen={handleDropdownBeforeOpen} + renderOption={(item) => ( +
+ {item} +
+ )} + getOptionValue={(item) => item} + getDisplayValue={(item) => ( +
+ {item} +
+ )} + notFound={
{t('graphPanel.graphLabels.noLabels')}
} + ariaLabel={t('graphPanel.graphLabels.label')} + placeholder={t('graphPanel.graphLabels.placeholder')} + searchPlaceholder={t('graphPanel.graphLabels.placeholder')} + noResultsMessage={t('graphPanel.graphLabels.noLabels')} + value={label !== null ? label : '*'} + onChange={(newLabel) => { + const currentLabel = useSettingsStore.getState().queryLabel; - // select the last item means query all - if (newLabel === '...') { - newLabel = '*'; - } + // select the last item means query all + if (newLabel === '...') { + newLabel = '*'; + } - // Handle reselecting the same label - if (newLabel === currentLabel && newLabel !== '*') { - newLabel = '*'; - } + // Handle reselecting the same label + if (newLabel === currentLabel && newLabel !== '*') { + newLabel = '*'; + } - // Add selected label to search history (except for special cases) - if (newLabel && newLabel !== '*' && newLabel !== '...' && newLabel.trim() !== '') { - SearchHistoryManager.addToHistory(newLabel); - } + // Add selected label to search history (except for special cases) + if (newLabel && newLabel !== '*' && newLabel !== '...' && newLabel.trim() !== '') { + SearchHistoryManager.addToHistory(newLabel); + } - // Reset graphDataFetchAttempted flag to ensure data fetch is triggered - useGraphStore.getState().setGraphDataFetchAttempted(false); + // Reset graphDataFetchAttempted flag to ensure data fetch is triggered + useGraphStore.getState().setGraphDataFetchAttempted(false); - // Update the label to trigger data loading - useSettingsStore.getState().setQueryLabel(newLabel); - }} - clearable={false} // Prevent clearing value on reselect - debounceTime={500} - /> + // Update the label to trigger data loading + useSettingsStore.getState().setQueryLabel(newLabel); + + // Force graph re-render and reset zoom/scale (must be AFTER setQueryLabel) + useGraphStore.getState().incrementGraphDataVersion(); + }} + clearable={false} // Prevent clearing value on reselect + debounceTime={500} + /> +
) } 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 ( - +