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:
parent
699ca3ba00
commit
71367c7bc2
4 changed files with 247 additions and 30 deletions
|
|
@ -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">
|
||||
|
|
|
|||
168
lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx
Normal file
168
lightrag_webui/src/components/ui/UserPromptInputWithHistory.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue