From 71367c7bc2d1bc4499a8b3c212eb00727c4c3a6a Mon Sep 17 00:00:00 2001 From: yangdx Date: Thu, 25 Sep 2025 06:16:32 +0800 Subject: [PATCH] Add user prompt history dropdown to query settings - Create UserPromptInputWithHistory component - Move user prompt field to top of panel - Add history tracking to settings store - Include keyboard navigation support - Auto-save prompts on query execution --- .../components/retrieval/QuerySettings.tsx | 63 ++++--- .../ui/UserPromptInputWithHistory.tsx | 168 ++++++++++++++++++ .../src/features/RetrievalTesting.tsx | 5 + lightrag_webui/src/stores/settings.ts | 41 ++++- 4 files changed, 247 insertions(+), 30 deletions(-) create mode 100644 lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx diff --git a/lightrag_webui/src/components/retrieval/QuerySettings.tsx b/lightrag_webui/src/components/retrieval/QuerySettings.tsx index ddcfe031..470b4e04 100644 --- a/lightrag_webui/src/components/retrieval/QuerySettings.tsx +++ b/lightrag_webui/src/components/retrieval/QuerySettings.tsx @@ -3,6 +3,7 @@ import { QueryMode, QueryRequest } from '@/api/lightrag' // Removed unused import for Text component import Checkbox from '@/components/ui/Checkbox' import Input from '@/components/ui/Input' +import UserPromptInputWithHistory from '@/components/ui/UserPromptInputWithHistory' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card' import { Select, @@ -20,11 +21,16 @@ import { RotateCcw } from 'lucide-react' export default function QuerySettings() { const { t } = useTranslation() const querySettings = useSettingsStore((state) => state.querySettings) + const userPromptHistory = useSettingsStore((state) => state.userPromptHistory) const handleChange = useCallback((key: keyof QueryRequest, value: any) => { useSettingsStore.getState().updateQuerySettings({ [key]: value }) }, []) + const handleSelectFromHistory = useCallback((prompt: string) => { + handleChange('user_prompt', prompt) + }, [handleChange]) + // Default values for reset functionality const defaultValues = useMemo(() => ({ mode: 'mix' as QueryMode, @@ -62,14 +68,41 @@ export default function QuerySettings() { ) return ( - + {t('retrievePanel.querySettings.parametersTitle')} {t('retrievePanel.querySettings.parametersDescription')}
-
+
+ {/* User Prompt - Moved to top for better dropdown space */} + <> + + + + + + +

{t('retrievePanel.querySettings.userPromptTooltip')}

+
+
+
+
+ handleChange('user_prompt', value)} + onSelectFromHistory={handleSelectFromHistory} + history={userPromptHistory} + placeholder={t('retrievePanel.querySettings.userPromptPlaceholder')} + className="h-9" + /> +
+ + {/* Query Mode */} <> @@ -353,32 +386,6 @@ export default function QuerySettings() {
- - {/* User Prompt */} - <> - - - - - - -

{t('retrievePanel.querySettings.userPromptTooltip')}

-
-
-
-
- handleChange('user_prompt', e.target.value)} - placeholder={t('retrievePanel.querySettings.userPromptPlaceholder')} - className="h-9" - /> -
- - {/* Toggle Options */} <>
diff --git a/lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx b/lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx new file mode 100644 index 00000000..d9fedb4b --- /dev/null +++ b/lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx @@ -0,0 +1,168 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react' +import { ChevronDown } from 'lucide-react' +import { cn } from '@/lib/utils' +import Input from './Input' + +interface UserPromptInputWithHistoryProps { + value: string + onChange: (value: string) => void + placeholder?: string + className?: string + id?: string + history: string[] + onSelectFromHistory: (prompt: string) => void +} + +export default function UserPromptInputWithHistory({ + value, + onChange, + placeholder, + className, + id, + history, + onSelectFromHistory +}: UserPromptInputWithHistoryProps) { + const [isOpen, setIsOpen] = useState(false) + const [selectedIndex, setSelectedIndex] = useState(-1) + const [isHovered, setIsHovered] = useState(false) + const dropdownRef = useRef(null) + const inputRef = useRef(null) + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + setSelectedIndex(-1) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + // Handle keyboard navigation + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (!isOpen) { + if (e.key === 'ArrowDown' && history.length > 0) { + e.preventDefault() + setIsOpen(true) + setSelectedIndex(0) + } + return + } + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setSelectedIndex(prev => + prev < history.length - 1 ? prev + 1 : prev + ) + break + case 'ArrowUp': + e.preventDefault() + setSelectedIndex(prev => prev > 0 ? prev - 1 : -1) + if (selectedIndex === 0) { + setSelectedIndex(-1) + } + break + case 'Enter': + if (selectedIndex >= 0 && selectedIndex < history.length) { + e.preventDefault() + const selectedPrompt = history[selectedIndex] + onSelectFromHistory(selectedPrompt) + setIsOpen(false) + setSelectedIndex(-1) + } + break + case 'Escape': + e.preventDefault() + setIsOpen(false) + setSelectedIndex(-1) + break + } + }, [isOpen, selectedIndex, history, onSelectFromHistory]) + + const handleInputClick = () => { + if (history.length > 0) { + setIsOpen(!isOpen) + setSelectedIndex(-1) + } + } + + const handleDropdownItemClick = (prompt: string) => { + onSelectFromHistory(prompt) + setIsOpen(false) + setSelectedIndex(-1) + inputRef.current?.focus() + } + + const handleInputChange = (e: React.ChangeEvent) => { + onChange(e.target.value) + } + + const handleMouseEnter = () => { + setIsHovered(true) + } + + const handleMouseLeave = () => { + setIsHovered(false) + } + + return ( +
+
+ 0 ? 'pr-5' : 'pr-2', 'w-full', className)} + /> + {isHovered && history.length > 0 && ( + + )} +
+ + {/* Dropdown */} + {isOpen && history.length > 0 && ( +
+ {history.map((prompt, index) => ( + + ))} +
+ )} +
+ ) +} diff --git a/lightrag_webui/src/features/RetrievalTesting.tsx b/lightrag_webui/src/features/RetrievalTesting.tsx index 5abeaf63..6db83390 100644 --- a/lightrag_webui/src/features/RetrievalTesting.tsx +++ b/lightrag_webui/src/features/RetrievalTesting.tsx @@ -313,6 +313,11 @@ export default function RetrievalTesting() { // Prepare query parameters const state = useSettingsStore.getState() + // Add user prompt to history if it exists and is not empty + if (state.querySettings.user_prompt && state.querySettings.user_prompt.trim()) { + state.addUserPromptToHistory(state.querySettings.user_prompt.trim()) + } + // Determine the effective mode const effectiveMode = modeOverride || state.querySettings.mode diff --git a/lightrag_webui/src/stores/settings.ts b/lightrag_webui/src/stores/settings.ts index 866504d0..e5761fc1 100644 --- a/lightrag_webui/src/stores/settings.ts +++ b/lightrag_webui/src/stores/settings.ts @@ -16,6 +16,11 @@ interface SettingsState { documentsPageSize: number setDocumentsPageSize: (size: number) => void + // User prompt history + userPromptHistory: string[] + addUserPromptToHistory: (prompt: string) => void + setUserPromptHistory: (history: string[]) => void + // Graph viewer settings showPropertyPanel: boolean showNodeSearchBar: boolean @@ -110,6 +115,7 @@ const useSettingsStoreBase = create()( documentsPageSize: 10, retrievalHistory: [], + userPromptHistory: [], querySettings: { mode: 'global', @@ -196,12 +202,39 @@ const useSettingsStoreBase = create()( setShowFileName: (show: boolean) => set({ showFileName: show }), setShowLegend: (show: boolean) => set({ showLegend: show }), - setDocumentsPageSize: (size: number) => set({ documentsPageSize: size }) + setDocumentsPageSize: (size: number) => set({ documentsPageSize: size }), + + // User prompt history methods + addUserPromptToHistory: (prompt: string) => { + if (!prompt.trim()) return + + set((state) => { + const newHistory = [...state.userPromptHistory] + + // Remove existing occurrence if found + const existingIndex = newHistory.indexOf(prompt) + if (existingIndex !== -1) { + newHistory.splice(existingIndex, 1) + } + + // Add to beginning + newHistory.unshift(prompt) + + // Keep only last 10 items + if (newHistory.length > 10) { + newHistory.splice(10) + } + + return { userPromptHistory: newHistory } + }) + }, + + setUserPromptHistory: (history: string[]) => set({ userPromptHistory: history }) }), { name: 'settings-storage', storage: createJSONStorage(() => localStorage), - version: 17, + version: 18, migrate: (state: any, version: number) => { if (version < 2) { state.showEdgeLabel = false @@ -294,6 +327,10 @@ const useSettingsStoreBase = create()( state.querySettings.history_turns = 0 } } + if (version < 18) { + // Add userPromptHistory field for older versions + state.userPromptHistory = [] + } return state } }