add query settings

This commit is contained in:
ArnoChen
2025-02-16 21:43:14 +08:00
parent d1358c30f3
commit fc3ca2777f
15 changed files with 757 additions and 102 deletions

View File

@@ -10,6 +10,7 @@
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
@@ -37,6 +38,7 @@
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-number-format": "^5.4.3",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sigma": "^3.0.1", "sigma": "^3.0.1",
"sonner": "^1.7.4", "sonner": "^1.7.4",
@@ -255,6 +257,8 @@
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.3", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ=="], "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.3", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.1.6", "", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="], "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
@@ -899,6 +903,8 @@
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-number-format": ["react-number-format@5.4.3", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VCY5hFg/soBighAoGcdE+GagkJq0230qN6jcS5sp8wQX1qy1fYN/RX7/BXkrs0oyzzwqR8/+eSUrqXbGeywdUQ=="],
"react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], "react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag Graph Viewer</title> <title>Lightrag</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -16,6 +16,7 @@
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
@@ -43,6 +44,7 @@
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-number-format": "^5.4.3",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sigma": "^3.0.1", "sigma": "^3.0.1",
"sonner": "^1.7.4", "sonner": "^1.7.4",

View File

@@ -52,13 +52,52 @@ export type LightragDocumentsScanProgress = {
progress: number progress: number
} }
/**
* Specifies the retrieval mode:
* - "naive": Performs a basic search without advanced techniques.
* - "local": Focuses on context-dependent information.
* - "global": Utilizes global knowledge.
* - "hybrid": Combines local and global retrieval methods.
* - "mix": Integrates knowledge graph and vector retrieval.
*/
export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix' export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
export type Message = {
role: 'user' | 'assistant' | 'system'
content: string
}
export type QueryRequest = { export type QueryRequest = {
query: string query: string
/** Specifies the retrieval mode. */
mode: QueryMode mode: QueryMode
stream?: boolean /** If True, only returns the retrieved context without generating a response. */
only_need_context?: boolean only_need_context?: boolean
/** If True, only returns the generated prompt without producing a response. */
only_need_prompt?: boolean
/** Defines the response format. Examples: 'Multiple Paragraphs', 'Single Paragraph', 'Bullet Points'. */
response_type?: string
/** If True, enables streaming output for real-time responses. */
stream?: boolean
/** Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode. */
top_k?: number
/** Maximum number of tokens allowed for each retrieved text chunk. */
max_token_for_text_unit?: number
/** Maximum number of tokens allocated for relationship descriptions in global retrieval. */
max_token_for_global_context?: number
/** Maximum number of tokens allocated for entity descriptions in local retrieval. */
max_token_for_local_context?: number
/** List of high-level keywords to prioritize in retrieval. */
hl_keywords?: string[]
/** List of low-level keywords to refine retrieval focus. */
ll_keywords?: string[]
/**
* Stores past conversation history to maintain context.
* Format: [{"role": "user/assistant", "content": "message"}].
*/
conversation_history?: Message[]
/** Number of complete conversation turns (user-assistant pairs) to consider in the response context. */
history_turns?: number
} }
export type QueryResponse = { export type QueryResponse = {
@@ -68,7 +107,6 @@ export type QueryResponse = {
export type DocumentActionResponse = { export type DocumentActionResponse = {
status: 'success' | 'partial_success' | 'failure' status: 'success' | 'partial_success' | 'failure'
message: string message: string
document_count: number
} }
export const InvalidApiKeyError = 'Invalid API Key' export const InvalidApiKeyError = 'Invalid API Key'

View File

@@ -1,5 +1,5 @@
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
import { Checkbox } from '@/components/ui/Checkbox' import Checkbox from '@/components/ui/Checkbox'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import Separator from '@/components/ui/Separator' import Separator from '@/components/ui/Separator'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'

View File

@@ -0,0 +1,279 @@
import { useCallback } from 'react'
import { QueryMode, QueryRequest } from '@/api/lightrag'
import Text from '@/components/ui/Text'
import Input from '@/components/ui/Input'
import Checkbox from '@/components/ui/Checkbox'
import NumberInput from '@/components/ui/NumberInput'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/Select'
import { useSettingsStore } from '@/stores/settings'
export default function QuerySettings() {
const querySettings = useSettingsStore((state) => state.querySettings)
const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
useSettingsStore.getState().updateQuerySettings({ [key]: value })
}, [])
return (
<Card className="flex shrink-0 flex-col">
<CardHeader className="px-4 pt-4 pb-2">
<CardTitle>Parameters</CardTitle>
<CardDescription>Configure your query parameters</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">
{/* Query Mode */}
<>
<Text
className="ml-1"
text="Query Mode"
tooltip="Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval"
side="left"
/>
<Select
value={querySettings.mode}
onValueChange={(v) => handleChange('mode', v as QueryMode)}
>
<SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="naive">Naive</SelectItem>
<SelectItem value="local">Local</SelectItem>
<SelectItem value="global">Global</SelectItem>
<SelectItem value="hybrid">Hybrid</SelectItem>
<SelectItem value="mix">Mix</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</>
{/* Response Format */}
<>
<Text
className="ml-1"
text="Response Format"
tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
side="left"
/>
<Select
value={querySettings.response_type}
onValueChange={(v) => handleChange('response_type', v)}
>
<SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Multiple Paragraphs">Single Paragraph</SelectItem>
<SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
<SelectItem value="Bullet Points">Bullet Points</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</>
{/* Top K */}
<>
<Text
className="ml-1"
text="Top K Results"
tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
side="left"
/>
<NumberInput
id="top_k"
stepper={1}
value={querySettings.top_k}
onValueChange={(v) => handleChange('top_k', v)}
min={1}
placeholder="Number of results"
/>
</>
{/* Max Tokens */}
<>
<>
<Text
className="ml-1"
text="Max Tokens for Text Unit"
tooltip="Maximum number of tokens allowed for each retrieved text chunk"
side="left"
/>
<NumberInput
id="max_token_for_text_unit"
stepper={500}
value={querySettings.max_token_for_text_unit}
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
min={1}
placeholder="Max tokens for text unit"
/>
</>
<>
<Text
text="Max Tokens for Global Context"
tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
side="left"
/>
<NumberInput
id="max_token_for_global_context"
stepper={500}
value={querySettings.max_token_for_global_context}
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
min={1}
placeholder="Max tokens for global context"
/>
</>
<>
<Text
className="ml-1"
text="Max Tokens for Local Context"
tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
side="left"
/>
<NumberInput
id="max_token_for_local_context"
stepper={500}
value={querySettings.max_token_for_local_context}
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
min={1}
placeholder="Max tokens for local context"
/>
</>
</>
{/* History Turns */}
<>
<Text
className="ml-1"
text="History Turns"
tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
side="left"
/>
<NumberInput
className="!border-input"
id="history_turns"
stepper={1}
type="text"
value={querySettings.history_turns}
onValueChange={(v) => handleChange('history_turns', v)}
min={0}
placeholder="Number of history turns"
/>
</>
{/* Keywords */}
<>
<>
<Text
className="ml-1"
text="High-Level Keywords"
tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
side="left"
/>
<Input
id="hl_keywords"
type="text"
value={querySettings.hl_keywords?.join(', ')}
onChange={(e) => {
const keywords = e.target.value
.split(',')
.map((k) => k.trim())
.filter((k) => k !== '')
handleChange('hl_keywords', keywords)
}}
placeholder="Enter keywords"
/>
</>
<>
<Text
className="ml-1"
text="Low-Level Keywords"
tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
side="left"
/>
<Input
id="ll_keywords"
type="text"
value={querySettings.ll_keywords?.join(', ')}
onChange={(e) => {
const keywords = e.target.value
.split(',')
.map((k) => k.trim())
.filter((k) => k !== '')
handleChange('ll_keywords', keywords)
}}
placeholder="Enter keywords"
/>
</>
</>
{/* Toggle Options */}
<>
<div className="flex items-center gap-2">
<Text
className="ml-1"
text="Only Need Context"
tooltip="If True, only returns the retrieved context without generating a response"
side="left"
/>
<div className="grow" />
<Checkbox
className="mr-1 cursor-pointer"
id="only_need_context"
checked={querySettings.only_need_context}
onCheckedChange={(checked) => handleChange('only_need_context', checked)}
/>
</div>
<div className="flex items-center gap-2">
<Text
className="ml-1"
text="Only Need Prompt"
tooltip="If True, only returns the generated prompt without producing a response"
side="left"
/>
<div className="grow" />
<Checkbox
className="mr-1 cursor-pointer"
id="only_need_prompt"
checked={querySettings.only_need_prompt}
onCheckedChange={(checked) => handleChange('only_need_prompt', checked)}
/>
</div>
<div className="flex items-center gap-2">
<Text
className="ml-1"
text="Stream Response"
tooltip="If True, enables streaming output for real-time responses"
side="left"
/>
<div className="grow" />
<Checkbox
className="mr-1 cursor-pointer"
id="stream"
checked={querySettings.stream}
onCheckedChange={(checked) => handleChange('stream', checked)}
/>
</div>
</>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@@ -23,4 +23,4 @@ const Checkbox = React.forwardRef<
)) ))
Checkbox.displayName = CheckboxPrimitive.Root.displayName Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox } export default Checkbox

View File

@@ -0,0 +1,131 @@
import { ChevronDown, ChevronUp } from 'lucide-react'
import { forwardRef, useCallback, useEffect, useState } from 'react'
import { NumericFormat, NumericFormatProps } from 'react-number-format'
import Button from '@/components/ui/Button'
import Input from '@/components/ui/Input'
import { cn } from '@/lib/utils'
export interface NumberInputProps extends Omit<NumericFormatProps, 'value' | 'onValueChange'> {
stepper?: number
thousandSeparator?: string
placeholder?: string
defaultValue?: number
min?: number
max?: number
value?: number // Controlled value
suffix?: string
prefix?: string
onValueChange?: (value: number | undefined) => void
fixedDecimalScale?: boolean
decimalScale?: number
}
const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
(
{
stepper,
thousandSeparator,
placeholder,
defaultValue,
min = -Infinity,
max = Infinity,
onValueChange,
fixedDecimalScale = false,
decimalScale = 0,
className = undefined,
suffix,
prefix,
value: controlledValue,
...props
},
ref
) => {
const [value, setValue] = useState<number | undefined>(controlledValue ?? defaultValue)
const handleIncrement = useCallback(() => {
setValue((prev) =>
prev === undefined ? (stepper ?? 1) : Math.min(prev + (stepper ?? 1), max)
)
}, [stepper, max])
const handleDecrement = useCallback(() => {
setValue((prev) =>
prev === undefined ? -(stepper ?? 1) : Math.max(prev - (stepper ?? 1), min)
)
}, [stepper, min])
useEffect(() => {
if (controlledValue !== undefined) {
setValue(controlledValue)
}
}, [controlledValue])
const handleChange = (values: { value: string; floatValue: number | undefined }) => {
const newValue = values.floatValue === undefined ? undefined : values.floatValue
setValue(newValue)
if (onValueChange) {
onValueChange(newValue)
}
}
const handleBlur = () => {
if (value !== undefined) {
if (value < min) {
setValue(min)
;(ref as React.RefObject<HTMLInputElement>).current!.value = String(min)
} else if (value > max) {
setValue(max)
;(ref as React.RefObject<HTMLInputElement>).current!.value = String(max)
}
}
}
return (
<div className="relative flex">
<NumericFormat
value={value}
onValueChange={handleChange}
thousandSeparator={thousandSeparator}
decimalScale={decimalScale}
fixedDecimalScale={fixedDecimalScale}
allowNegative={min < 0}
valueIsNumericString
onBlur={handleBlur}
max={max}
min={min}
suffix={suffix}
prefix={prefix}
customInput={(props) => <Input {...props} className={cn('w-full', className)} />}
placeholder={placeholder}
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
getInputRef={ref}
{...props}
/>
<div className="absolute top-0 right-0 bottom-0 flex flex-col">
<Button
aria-label="Increase value"
className="border-input h-1/2 rounded-l-none rounded-br-none border-b border-l px-2 focus-visible:relative"
variant="outline"
onClick={handleIncrement}
disabled={value === max}
>
<ChevronUp size={15} />
</Button>
<Button
aria-label="Decrease value"
className="border-input h-1/2 rounded-l-none rounded-tr-none border-b border-l px-2 focus-visible:relative"
variant="outline"
onClick={handleDecrement}
disabled={value === min}
>
<ChevronDown size={15} />
</Button>
</div>
</div>
)
}
)
NumberInput.displayName = 'NumberInput'
export default NumberInput

View File

@@ -0,0 +1,151 @@
import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { Check, ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn('py-1.5 pr-2 pl-8 text-sm font-semibold', className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('bg-muted -mx-1 my-1 h-px', className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton
}

View File

@@ -8,19 +8,31 @@ const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger const TooltipTrigger = TooltipPrimitive.Trigger
const processTooltipContent = (content: string) => {
if (typeof content !== 'string') return content
return content.split('\\n').map((line, i) => (
<React.Fragment key={i}>
{line}
{i < content.split('\\n').length - 1 && <br />}
</React.Fragment>
))
}
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ComponentRef<typeof TooltipPrimitive.Content>, React.ComponentRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, children, ...props }, ref) => (
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md', 'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1 max-w-sm overflow-hidden rounded-md border px-3 py-2 text-sm shadow-md',
className className
)} )}
{...props} {...props}
/> >
{typeof children === 'string' ? processTooltipContent(children) : children}
</TooltipPrimitive.Content>
)) ))
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName

View File

@@ -11,8 +11,8 @@ import {
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card' import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
import Progress from '@/components/ui/Progress' import Progress from '@/components/ui/Progress'
import EmptyCard from '@/components/ui/EmptyCard' import EmptyCard from '@/components/ui/EmptyCard'
import UploadDocumentsDialog from '@/components/document/UploadDocumentsDialog' import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
import ClearDocumentsDialog from '@/components/document/ClearDocumentsDialog' import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
import { import {
getDocuments, getDocuments,

View File

@@ -1,17 +1,13 @@
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { queryTextStream, QueryMode } from '@/api/lightrag' import { queryText, queryTextStream, Message } from '@/api/lightrag'
import { errorMessage } from '@/lib/utils' import { errorMessage } from '@/lib/utils'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useDebounce } from '@/hooks/useDebounce' import { useDebounce } from '@/hooks/useDebounce'
import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react' import QuerySettings from '@/components/retrieval/QuerySettings'
type Message = { import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react'
id: string
content: string
role: 'User' | 'LightRAG'
}
export default function RetrievalTesting() { export default function RetrievalTesting() {
const [messages, setMessages] = useState<Message[]>( const [messages, setMessages] = useState<Message[]>(
@@ -19,7 +15,6 @@ export default function RetrievalTesting() {
) )
const [inputValue, setInputValue] = useState('') const [inputValue, setInputValue] = useState('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [mode, setMode] = useState<QueryMode>('mix')
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const scrollToBottom = useCallback(() => { const scrollToBottom = useCallback(() => {
@@ -31,23 +26,24 @@ export default function RetrievalTesting() {
e.preventDefault() e.preventDefault()
if (!inputValue.trim() || isLoading) return if (!inputValue.trim() || isLoading) return
// Create messages
const userMessage: Message = { const userMessage: Message = {
id: Date.now().toString(),
content: inputValue, content: inputValue,
role: 'User' role: 'user'
} }
const assistantMessage: Message = { const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
content: '', content: '',
role: 'LightRAG' role: 'assistant'
} }
// Add messages to chatbox
setMessages((prev) => { setMessages((prev) => {
const newMessages = [...prev, userMessage, assistantMessage] const newMessages = [...prev, userMessage, assistantMessage]
return newMessages return newMessages
}) })
// Clear input and set loading
setInputValue('') setInputValue('')
setIsLoading(true) setIsLoading(true)
@@ -57,25 +53,34 @@ export default function RetrievalTesting() {
setMessages((prev) => { setMessages((prev) => {
const newMessages = [...prev] const newMessages = [...prev]
const lastMessage = newMessages[newMessages.length - 1] const lastMessage = newMessages[newMessages.length - 1]
if (lastMessage.role === 'LightRAG') { if (lastMessage.role === 'assistant') {
lastMessage.content = assistantMessage.content lastMessage.content = assistantMessage.content
} }
return newMessages return newMessages
}) })
} }
try { // Prepare query parameters
await queryTextStream( const state = useSettingsStore.getState()
{ const queryParams = {
...state.querySettings,
query: userMessage.content, query: userMessage.content,
mode: mode, conversation_history: messages
stream: true }
},
updateAssistantMessage try {
) // Run query
if (state.querySettings.stream) {
await queryTextStream(queryParams, updateAssistantMessage)
} else {
const response = await queryText(queryParams)
updateAssistantMessage(response.response)
}
} catch (err) { } catch (err) {
// Handle error
updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`) updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`)
} finally { } finally {
// Clear loading and add messages to state
setIsLoading(false) setIsLoading(false)
useSettingsStore useSettingsStore
.getState() .getState()
@@ -86,7 +91,7 @@ export default function RetrievalTesting() {
]) ])
} }
}, },
[inputValue, isLoading, mode, setMessages] [inputValue, isLoading, messages, setMessages]
) )
const debouncedMessages = useDebounce(messages, 100) const debouncedMessages = useDebounce(messages, 100)
@@ -98,7 +103,8 @@ export default function RetrievalTesting() {
}, [setMessages]) }, [setMessages])
return ( return (
<div className="flex size-full flex-col gap-4 px-32 py-6"> <div className="flex size-full gap-2 px-2 pb-12">
<div className="flex grow flex-col gap-4">
<div className="relative grow"> <div className="relative grow">
<div className="bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2"> <div className="bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2">
<div className="flex min-h-0 flex-1 flex-col gap-2"> <div className="flex min-h-0 flex-1 flex-col gap-2">
@@ -107,14 +113,14 @@ export default function RetrievalTesting() {
Start a retrieval by typing your query below Start a retrieval by typing your query below
</div> </div>
) : ( ) : (
messages.map((message) => ( messages.map((message, idx) => (
<div <div
key={message.id} key={idx}
className={`flex ${message.role === 'User' ? 'justify-end' : 'justify-start'}`} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
> >
<div <div
className={`max-w-[80%] rounded-lg px-4 py-2 ${ className={`max-w-[80%] rounded-lg px-4 py-2 ${
message.role === 'User' ? 'bg-primary text-primary-foreground' : 'bg-muted' message.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'
}`} }`}
> >
<pre className="break-words whitespace-pre-wrap">{message.content}</pre> <pre className="break-words whitespace-pre-wrap">{message.content}</pre>
@@ -130,7 +136,7 @@ export default function RetrievalTesting() {
</div> </div>
</div> </div>
<form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2 pb-2"> <form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -141,18 +147,6 @@ export default function RetrievalTesting() {
<EraserIcon /> <EraserIcon />
Clear Clear
</Button> </Button>
<select
className="border-input bg-background ring-offset-background h-9 rounded-md border px-3 py-1 text-sm"
value={mode}
onChange={(e) => setMode(e.target.value as QueryMode)}
disabled={isLoading}
>
<option value="naive">Naive</option>
<option value="local">Local</option>
<option value="global">Global</option>
<option value="hybrid">Hybrid</option>
<option value="mix">Mix</option>
</select>
<Input <Input
className="flex-1" className="flex-1"
value={inputValue} value={inputValue}
@@ -166,5 +160,8 @@ export default function RetrievalTesting() {
</Button> </Button>
</form> </form>
</div> </div>
<QuerySettings />
</div>
) )
} }

View File

@@ -2,6 +2,7 @@ import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware' import { persist, createJSONStorage } from 'zustand/middleware'
import { createSelectors } from '@/lib/utils' import { createSelectors } from '@/lib/utils'
import { defaultQueryLabel } from '@/lib/constants' import { defaultQueryLabel } from '@/lib/constants'
import { Message, QueryRequest } from '@/api/lightrag'
type Theme = 'dark' | 'light' | 'system' type Theme = 'dark' | 'light' | 'system'
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' type Tab = 'documents' | 'knowledge-graph' | 'retrieval'
@@ -32,8 +33,11 @@ interface SettingsState {
currentTab: Tab currentTab: Tab
setCurrentTab: (tab: Tab) => void setCurrentTab: (tab: Tab) => void
retrievalHistory: any[] retrievalHistory: Message[]
setRetrievalHistory: (history: any[]) => void setRetrievalHistory: (history: Message[]) => void
querySettings: Omit<QueryRequest, 'query'>
updateQuerySettings: (settings: Partial<QueryRequest>) => void
} }
const useSettingsStoreBase = create<SettingsState>()( const useSettingsStoreBase = create<SettingsState>()(
@@ -60,6 +64,21 @@ const useSettingsStoreBase = create<SettingsState>()(
retrievalHistory: [], retrievalHistory: [],
querySettings: {
mode: 'global',
response_type: 'Multiple Paragraphs',
top_k: 10,
max_token_for_text_unit: 4000,
max_token_for_global_context: 4000,
max_token_for_local_context: 4000,
only_need_context: false,
only_need_prompt: false,
stream: true,
history_turns: 3,
hl_keywords: [],
ll_keywords: []
},
setTheme: (theme: Theme) => set({ theme }), setTheme: (theme: Theme) => set({ theme }),
setQueryLabel: (queryLabel: string) => setQueryLabel: (queryLabel: string) =>
@@ -73,15 +92,17 @@ const useSettingsStoreBase = create<SettingsState>()(
setCurrentTab: (tab: Tab) => set({ currentTab: tab }), setCurrentTab: (tab: Tab) => set({ currentTab: tab }),
setRetrievalHistory: (history: any[]) => set({ retrievalHistory: history }) setRetrievalHistory: (history: Message[]) => set({ retrievalHistory: history }),
updateQuerySettings: (settings: Partial<QueryRequest>) =>
set((state) => ({
querySettings: { ...state.querySettings, ...settings }
}))
}), }),
{ {
name: 'settings-storage', name: 'settings-storage',
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
version: 5, version: 6,
partialize(state) {
return { ...state, retrievalHistory: undefined }
},
migrate: (state: any, version: number) => { migrate: (state: any, version: number) => {
if (version < 2) { if (version < 2) {
state.showEdgeLabel = false state.showEdgeLabel = false
@@ -99,6 +120,24 @@ const useSettingsStoreBase = create<SettingsState>()(
if (version < 5) { if (version < 5) {
state.currentTab = 'documents' state.currentTab = 'documents'
} }
if (version < 6) {
state.querySettings = {
mode: 'global',
response_type: 'Multiple Paragraphs',
top_k: 10,
max_token_for_text_unit: 4000,
max_token_for_global_context: 4000,
max_token_for_local_context: 4000,
only_need_context: false,
only_need_prompt: false,
stream: true,
history_turns: 3,
hl_keywords: [],
ll_keywords: []
}
state.retrievalHistory = []
}
return state
} }
} }
) )