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
|
// Removed unused import for Text component
|
||||||
import Checkbox from '@/components/ui/Checkbox'
|
import Checkbox from '@/components/ui/Checkbox'
|
||||||
import Input from '@/components/ui/Input'
|
import Input from '@/components/ui/Input'
|
||||||
|
import UserPromptInputWithHistory from '@/components/ui/UserPromptInputWithHistory'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -20,11 +21,16 @@ import { RotateCcw } from 'lucide-react'
|
||||||
export default function QuerySettings() {
|
export default function QuerySettings() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const querySettings = useSettingsStore((state) => state.querySettings)
|
const querySettings = useSettingsStore((state) => state.querySettings)
|
||||||
|
const userPromptHistory = useSettingsStore((state) => state.userPromptHistory)
|
||||||
|
|
||||||
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
|
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
|
||||||
useSettingsStore.getState().updateQuerySettings({ [key]: value })
|
useSettingsStore.getState().updateQuerySettings({ [key]: value })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleSelectFromHistory = useCallback((prompt: string) => {
|
||||||
|
handleChange('user_prompt', prompt)
|
||||||
|
}, [handleChange])
|
||||||
|
|
||||||
// Default values for reset functionality
|
// Default values for reset functionality
|
||||||
const defaultValues = useMemo(() => ({
|
const defaultValues = useMemo(() => ({
|
||||||
mode: 'mix' as QueryMode,
|
mode: 'mix' as QueryMode,
|
||||||
|
|
@ -62,14 +68,41 @@ export default function QuerySettings() {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
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">
|
<CardHeader className="px-4 pt-4 pb-2">
|
||||||
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
<CardTitle>{t('retrievePanel.querySettings.parametersTitle')}</CardTitle>
|
||||||
<CardDescription className="sr-only">{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
<CardDescription className="sr-only">{t('retrievePanel.querySettings.parametersDescription')}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
<CardContent className="m-0 flex grow flex-col p-0 text-xs">
|
||||||
<div className="relative size-full">
|
<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 */}
|
{/* Query Mode */}
|
||||||
<>
|
<>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
|
@ -353,32 +386,6 @@ export default function QuerySettings() {
|
||||||
</div>
|
</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 */}
|
{/* Toggle Options */}
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<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
|
// Prepare query parameters
|
||||||
const state = useSettingsStore.getState()
|
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
|
// Determine the effective mode
|
||||||
const effectiveMode = modeOverride || state.querySettings.mode
|
const effectiveMode = modeOverride || state.querySettings.mode
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,11 @@ interface SettingsState {
|
||||||
documentsPageSize: number
|
documentsPageSize: number
|
||||||
setDocumentsPageSize: (size: number) => void
|
setDocumentsPageSize: (size: number) => void
|
||||||
|
|
||||||
|
// User prompt history
|
||||||
|
userPromptHistory: string[]
|
||||||
|
addUserPromptToHistory: (prompt: string) => void
|
||||||
|
setUserPromptHistory: (history: string[]) => void
|
||||||
|
|
||||||
// Graph viewer settings
|
// Graph viewer settings
|
||||||
showPropertyPanel: boolean
|
showPropertyPanel: boolean
|
||||||
showNodeSearchBar: boolean
|
showNodeSearchBar: boolean
|
||||||
|
|
@ -110,6 +115,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||||
documentsPageSize: 10,
|
documentsPageSize: 10,
|
||||||
|
|
||||||
retrievalHistory: [],
|
retrievalHistory: [],
|
||||||
|
userPromptHistory: [],
|
||||||
|
|
||||||
querySettings: {
|
querySettings: {
|
||||||
mode: 'global',
|
mode: 'global',
|
||||||
|
|
@ -196,12 +202,39 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||||
|
|
||||||
setShowFileName: (show: boolean) => set({ showFileName: show }),
|
setShowFileName: (show: boolean) => set({ showFileName: show }),
|
||||||
setShowLegend: (show: boolean) => set({ showLegend: 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',
|
name: 'settings-storage',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 17,
|
version: 18,
|
||||||
migrate: (state: any, version: number) => {
|
migrate: (state: any, version: number) => {
|
||||||
if (version < 2) {
|
if (version < 2) {
|
||||||
state.showEdgeLabel = false
|
state.showEdgeLabel = false
|
||||||
|
|
@ -294,6 +327,10 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||||
state.querySettings.history_turns = 0
|
state.querySettings.history_turns = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (version < 18) {
|
||||||
|
// Add userPromptHistory field for older versions
|
||||||
|
state.userPromptHistory = []
|
||||||
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue