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
This commit is contained in:
yangdx 2025-09-25 06:16:32 +08:00
parent 699ca3ba00
commit 71367c7bc2
4 changed files with 247 additions and 30 deletions

View file

@ -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 (
<Card className="flex shrink-0 flex-col min-w-[220px]">
<Card className="flex shrink-0 flex-col w-[280px]">
<CardHeader className="px-4 pt-4 pb-2">
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
<CardDescription className="sr-only">{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
</CardHeader>
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
<div className="relative size-full">
<div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2 pr-3">
<div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2 pr-2">
{/* User Prompt - Moved to top for better dropdown space */}
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<label htmlFor="user_prompt" className="ml-1 cursor-help">
{t('retrievePanel.querySettings.userPrompt')}
</label>
</TooltipTrigger>
<TooltipContent side="left">
<p>{t('retrievePanel.querySettings.userPromptTooltip')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<UserPromptInputWithHistory
id="user_prompt"
value={querySettings.user_prompt || ''}
onChange={(value) => handleChange('user_prompt', value)}
onSelectFromHistory={handleSelectFromHistory}
history={userPromptHistory}
placeholder={t('retrievePanel.querySettings.userPromptPlaceholder')}
className="h-9"
/>
</div>
</>
{/* Query Mode */}
<>
<TooltipProvider>
@ -353,32 +386,6 @@ export default function QuerySettings() {
</div>
</>
{/* User Prompt */}
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<label htmlFor="user_prompt" className="ml-1 cursor-help">
{t('retrievePanel.querySettings.userPrompt')}
</label>
</TooltipTrigger>
<TooltipContent side="left">
<p>{t('retrievePanel.querySettings.userPromptTooltip')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<Input
id="user_prompt"
value={querySettings.user_prompt}
onChange={(e) => handleChange('user_prompt', e.target.value)}
placeholder={t('retrievePanel.querySettings.userPromptPlaceholder')}
className="h-9"
/>
</div>
</>
{/* Toggle Options */}
<>
<div className="flex items-center gap-2">

View file

@ -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<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
onChange(e.target.value)
}
const handleMouseEnter = () => {
setIsHovered(true)
}
const handleMouseLeave = () => {
setIsHovered(false)
}
return (
<div className="relative" ref={dropdownRef} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div className="relative">
<Input
ref={inputRef}
id={id}
value={value}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onClick={handleInputClick}
placeholder={placeholder}
className={cn(isHovered && history.length > 0 ? 'pr-5' : 'pr-2', 'w-full', className)}
/>
{isHovered && history.length > 0 && (
<button
type="button"
onClick={handleInputClick}
className="absolute right-2 top-1/2 -translate-y-1/2 p-0 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
tabIndex={-1}
>
<ChevronDown
className={cn(
'h-3 w-3 transition-transform duration-200 text-gray-500',
isOpen && 'rotate-180'
)}
/>
</button>
)}
</div>
{/* Dropdown */}
{isOpen && history.length > 0 && (
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg max-h-60 overflow-auto min-w-0">
{history.map((prompt, index) => (
<button
key={index}
type="button"
onClick={() => handleDropdownItemClick(prompt)}
className={cn(
'w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
'border-b border-gray-100 dark:border-gray-700 last:border-b-0',
'focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700',
selectedIndex === index && 'bg-gray-100 dark:bg-gray-700'
)}
>
<div className="truncate" title={prompt}>
{prompt}
</div>
</button>
))}
</div>
)}
</div>
)
}

View file

@ -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

View file

@ -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<SettingsState>()(
documentsPageSize: 10,
retrievalHistory: [],
userPromptHistory: [],
querySettings: {
mode: 'global',
@ -196,12 +202,39 @@ const useSettingsStoreBase = create<SettingsState>()(
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<SettingsState>()(
state.querySettings.history_turns = 0
}
}
if (version < 18) {
// Add userPromptHistory field for older versions
state.userPromptHistory = []
}
return state
}
}