ArnoChen commited on
Commit
dff8a57
·
1 Parent(s): 29ee1c2

add query settings

Browse files
lightrag_webui/bun.lock CHANGED
@@ -10,6 +10,7 @@
10
  "@radix-ui/react-popover": "^1.1.6",
11
  "@radix-ui/react-progress": "^1.1.2",
12
  "@radix-ui/react-scroll-area": "^1.2.3",
 
13
  "@radix-ui/react-separator": "^1.1.2",
14
  "@radix-ui/react-slot": "^1.1.2",
15
  "@radix-ui/react-tabs": "^1.1.3",
@@ -37,6 +38,7 @@
37
  "react": "^19.0.0",
38
  "react-dom": "^19.0.0",
39
  "react-dropzone": "^14.3.5",
 
40
  "seedrandom": "^3.0.5",
41
  "sigma": "^3.0.1",
42
  "sonner": "^1.7.4",
@@ -255,6 +257,8 @@
255
 
256
  "@radix-ui/react-scroll-area": ["@radix-ui/[email protected]", "", { "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=="],
257
 
 
 
258
  "@radix-ui/react-separator": ["@radix-ui/[email protected]", "", { "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=="],
259
 
260
  "@radix-ui/react-slot": ["@radix-ui/[email protected]", "", { "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 @@
899
 
900
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
901
 
 
 
902
  "react-remove-scroll": ["[email protected]", "", { "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=="],
903
 
904
  "react-remove-scroll-bar": ["[email protected]", "", { "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=="],
 
10
  "@radix-ui/react-popover": "^1.1.6",
11
  "@radix-ui/react-progress": "^1.1.2",
12
  "@radix-ui/react-scroll-area": "^1.2.3",
13
+ "@radix-ui/react-select": "^2.1.6",
14
  "@radix-ui/react-separator": "^1.1.2",
15
  "@radix-ui/react-slot": "^1.1.2",
16
  "@radix-ui/react-tabs": "^1.1.3",
 
38
  "react": "^19.0.0",
39
  "react-dom": "^19.0.0",
40
  "react-dropzone": "^14.3.5",
41
+ "react-number-format": "^5.4.3",
42
  "seedrandom": "^3.0.5",
43
  "sigma": "^3.0.1",
44
  "sonner": "^1.7.4",
 
257
 
258
  "@radix-ui/react-scroll-area": ["@radix-ui/[email protected]", "", { "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=="],
259
 
260
+ "@radix-ui/react-select": ["@radix-ui/[email protected]", "", { "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=="],
261
+
262
  "@radix-ui/react-separator": ["@radix-ui/[email protected]", "", { "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=="],
263
 
264
  "@radix-ui/react-slot": ["@radix-ui/[email protected]", "", { "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=="],
 
903
 
904
  "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
905
 
906
+ "react-number-format": ["[email protected]", "", { "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=="],
907
+
908
  "react-remove-scroll": ["[email protected]", "", { "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=="],
909
 
910
  "react-remove-scroll-bar": ["[email protected]", "", { "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=="],
lightrag_webui/index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Lightrag Graph Viewer</title>
8
  </head>
9
  <body>
10
  <div id="root"></div>
 
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Lightrag</title>
8
  </head>
9
  <body>
10
  <div id="root"></div>
lightrag_webui/package.json CHANGED
@@ -16,6 +16,7 @@
16
  "@radix-ui/react-popover": "^1.1.6",
17
  "@radix-ui/react-progress": "^1.1.2",
18
  "@radix-ui/react-scroll-area": "^1.2.3",
 
19
  "@radix-ui/react-separator": "^1.1.2",
20
  "@radix-ui/react-slot": "^1.1.2",
21
  "@radix-ui/react-tabs": "^1.1.3",
@@ -43,6 +44,7 @@
43
  "react": "^19.0.0",
44
  "react-dom": "^19.0.0",
45
  "react-dropzone": "^14.3.5",
 
46
  "seedrandom": "^3.0.5",
47
  "sigma": "^3.0.1",
48
  "sonner": "^1.7.4",
 
16
  "@radix-ui/react-popover": "^1.1.6",
17
  "@radix-ui/react-progress": "^1.1.2",
18
  "@radix-ui/react-scroll-area": "^1.2.3",
19
+ "@radix-ui/react-select": "^2.1.6",
20
  "@radix-ui/react-separator": "^1.1.2",
21
  "@radix-ui/react-slot": "^1.1.2",
22
  "@radix-ui/react-tabs": "^1.1.3",
 
44
  "react": "^19.0.0",
45
  "react-dom": "^19.0.0",
46
  "react-dropzone": "^14.3.5",
47
+ "react-number-format": "^5.4.3",
48
  "seedrandom": "^3.0.5",
49
  "sigma": "^3.0.1",
50
  "sonner": "^1.7.4",
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -52,13 +52,52 @@ export type LightragDocumentsScanProgress = {
52
  progress: number
53
  }
54
 
 
 
 
 
 
 
 
 
55
  export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
56
 
 
 
 
 
 
57
  export type QueryRequest = {
58
  query: string
 
59
  mode: QueryMode
60
- stream?: boolean
61
  only_need_context?: boolean
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
 
64
  export type QueryResponse = {
@@ -68,7 +107,6 @@ export type QueryResponse = {
68
  export type DocumentActionResponse = {
69
  status: 'success' | 'partial_success' | 'failure'
70
  message: string
71
- document_count: number
72
  }
73
 
74
  export const InvalidApiKeyError = 'Invalid API Key'
 
52
  progress: number
53
  }
54
 
55
+ /**
56
+ * Specifies the retrieval mode:
57
+ * - "naive": Performs a basic search without advanced techniques.
58
+ * - "local": Focuses on context-dependent information.
59
+ * - "global": Utilizes global knowledge.
60
+ * - "hybrid": Combines local and global retrieval methods.
61
+ * - "mix": Integrates knowledge graph and vector retrieval.
62
+ */
63
  export type QueryMode = 'naive' | 'local' | 'global' | 'hybrid' | 'mix'
64
 
65
+ export type Message = {
66
+ role: 'user' | 'assistant' | 'system'
67
+ content: string
68
+ }
69
+
70
  export type QueryRequest = {
71
  query: string
72
+ /** Specifies the retrieval mode. */
73
  mode: QueryMode
74
+ /** If True, only returns the retrieved context without generating a response. */
75
  only_need_context?: boolean
76
+ /** If True, only returns the generated prompt without producing a response. */
77
+ only_need_prompt?: boolean
78
+ /** Defines the response format. Examples: 'Multiple Paragraphs', 'Single Paragraph', 'Bullet Points'. */
79
+ response_type?: string
80
+ /** If True, enables streaming output for real-time responses. */
81
+ stream?: boolean
82
+ /** Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode. */
83
+ top_k?: number
84
+ /** Maximum number of tokens allowed for each retrieved text chunk. */
85
+ max_token_for_text_unit?: number
86
+ /** Maximum number of tokens allocated for relationship descriptions in global retrieval. */
87
+ max_token_for_global_context?: number
88
+ /** Maximum number of tokens allocated for entity descriptions in local retrieval. */
89
+ max_token_for_local_context?: number
90
+ /** List of high-level keywords to prioritize in retrieval. */
91
+ hl_keywords?: string[]
92
+ /** List of low-level keywords to refine retrieval focus. */
93
+ ll_keywords?: string[]
94
+ /**
95
+ * Stores past conversation history to maintain context.
96
+ * Format: [{"role": "user/assistant", "content": "message"}].
97
+ */
98
+ conversation_history?: Message[]
99
+ /** Number of complete conversation turns (user-assistant pairs) to consider in the response context. */
100
+ history_turns?: number
101
  }
102
 
103
  export type QueryResponse = {
 
107
  export type DocumentActionResponse = {
108
  status: 'success' | 'partial_success' | 'failure'
109
  message: string
 
110
  }
111
 
112
  export const InvalidApiKeyError = 'Invalid API Key'
lightrag_webui/src/components/Settings.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
2
- import { Checkbox } from '@/components/ui/Checkbox'
3
  import Button from '@/components/ui/Button'
4
  import Separator from '@/components/ui/Separator'
5
  import Input from '@/components/ui/Input'
 
1
  import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
2
+ import Checkbox from '@/components/ui/Checkbox'
3
  import Button from '@/components/ui/Button'
4
  import Separator from '@/components/ui/Separator'
5
  import Input from '@/components/ui/Input'
lightrag_webui/src/components/{document → documents}/ClearDocumentsDialog.tsx RENAMED
File without changes
lightrag_webui/src/components/{document → documents}/UploadDocumentsDialog.tsx RENAMED
File without changes
lightrag_webui/src/components/retrieval/QuerySettings.tsx ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback } from 'react'
2
+ import { QueryMode, QueryRequest } from '@/api/lightrag'
3
+ import Text from '@/components/ui/Text'
4
+ import Input from '@/components/ui/Input'
5
+ import Checkbox from '@/components/ui/Checkbox'
6
+ import NumberInput from '@/components/ui/NumberInput'
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectGroup,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue
15
+ } from '@/components/ui/Select'
16
+ import { useSettingsStore } from '@/stores/settings'
17
+
18
+ export default function QuerySettings() {
19
+ const querySettings = useSettingsStore((state) => state.querySettings)
20
+
21
+ const handleChange = useCallback((key: keyof QueryRequest, value: any) => {
22
+ useSettingsStore.getState().updateQuerySettings({ [key]: value })
23
+ }, [])
24
+
25
+ return (
26
+ <Card className="flex shrink-0 flex-col">
27
+ <CardHeader className="px-4 pt-4 pb-2">
28
+ <CardTitle>Parameters</CardTitle>
29
+ <CardDescription>Configure your query parameters</CardDescription>
30
+ </CardHeader>
31
+ <CardContent className="m-0 flex grow flex-col p-0 text-xs">
32
+ <div className="relative size-full">
33
+ <div className="absolute inset-0 flex flex-col gap-2 overflow-auto px-2">
34
+ {/* Query Mode */}
35
+ <>
36
+ <Text
37
+ className="ml-1"
38
+ text="Query Mode"
39
+ 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"
40
+ side="left"
41
+ />
42
+ <Select
43
+ value={querySettings.mode}
44
+ onValueChange={(v) => handleChange('mode', v as QueryMode)}
45
+ >
46
+ <SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
47
+ <SelectValue />
48
+ </SelectTrigger>
49
+ <SelectContent>
50
+ <SelectGroup>
51
+ <SelectItem value="naive">Naive</SelectItem>
52
+ <SelectItem value="local">Local</SelectItem>
53
+ <SelectItem value="global">Global</SelectItem>
54
+ <SelectItem value="hybrid">Hybrid</SelectItem>
55
+ <SelectItem value="mix">Mix</SelectItem>
56
+ </SelectGroup>
57
+ </SelectContent>
58
+ </Select>
59
+ </>
60
+
61
+ {/* Response Format */}
62
+ <>
63
+ <Text
64
+ className="ml-1"
65
+ text="Response Format"
66
+ tooltip="Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points"
67
+ side="left"
68
+ />
69
+ <Select
70
+ value={querySettings.response_type}
71
+ onValueChange={(v) => handleChange('response_type', v)}
72
+ >
73
+ <SelectTrigger className="hover:bg-primary/5 h-9 cursor-pointer focus:ring-0 focus:ring-offset-0 focus:outline-0 active:right-0">
74
+ <SelectValue />
75
+ </SelectTrigger>
76
+ <SelectContent>
77
+ <SelectGroup>
78
+ <SelectItem value="Multiple Paragraphs">Single Paragraph</SelectItem>
79
+ <SelectItem value="Single Paragraph">Single Paragraph</SelectItem>
80
+ <SelectItem value="Bullet Points">Bullet Points</SelectItem>
81
+ </SelectGroup>
82
+ </SelectContent>
83
+ </Select>
84
+ </>
85
+
86
+ {/* Top K */}
87
+ <>
88
+ <Text
89
+ className="ml-1"
90
+ text="Top K Results"
91
+ tooltip="Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode"
92
+ side="left"
93
+ />
94
+ <NumberInput
95
+ id="top_k"
96
+ stepper={1}
97
+ value={querySettings.top_k}
98
+ onValueChange={(v) => handleChange('top_k', v)}
99
+ min={1}
100
+ placeholder="Number of results"
101
+ />
102
+ </>
103
+
104
+ {/* Max Tokens */}
105
+ <>
106
+ <>
107
+ <Text
108
+ className="ml-1"
109
+ text="Max Tokens for Text Unit"
110
+ tooltip="Maximum number of tokens allowed for each retrieved text chunk"
111
+ side="left"
112
+ />
113
+ <NumberInput
114
+ id="max_token_for_text_unit"
115
+ stepper={500}
116
+ value={querySettings.max_token_for_text_unit}
117
+ onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
118
+ min={1}
119
+ placeholder="Max tokens for text unit"
120
+ />
121
+ </>
122
+
123
+ <>
124
+ <Text
125
+ text="Max Tokens for Global Context"
126
+ tooltip="Maximum number of tokens allocated for relationship descriptions in global retrieval"
127
+ side="left"
128
+ />
129
+ <NumberInput
130
+ id="max_token_for_global_context"
131
+ stepper={500}
132
+ value={querySettings.max_token_for_global_context}
133
+ onValueChange={(v) => handleChange('max_token_for_global_context', v)}
134
+ min={1}
135
+ placeholder="Max tokens for global context"
136
+ />
137
+ </>
138
+
139
+ <>
140
+ <Text
141
+ className="ml-1"
142
+ text="Max Tokens for Local Context"
143
+ tooltip="Maximum number of tokens allocated for entity descriptions in local retrieval"
144
+ side="left"
145
+ />
146
+ <NumberInput
147
+ id="max_token_for_local_context"
148
+ stepper={500}
149
+ value={querySettings.max_token_for_local_context}
150
+ onValueChange={(v) => handleChange('max_token_for_local_context', v)}
151
+ min={1}
152
+ placeholder="Max tokens for local context"
153
+ />
154
+ </>
155
+ </>
156
+
157
+ {/* History Turns */}
158
+ <>
159
+ <Text
160
+ className="ml-1"
161
+ text="History Turns"
162
+ tooltip="Number of complete conversation turns (user-assistant pairs) to consider in the response context"
163
+ side="left"
164
+ />
165
+ <NumberInput
166
+ className="!border-input"
167
+ id="history_turns"
168
+ stepper={1}
169
+ type="text"
170
+ value={querySettings.history_turns}
171
+ onValueChange={(v) => handleChange('history_turns', v)}
172
+ min={0}
173
+ placeholder="Number of history turns"
174
+ />
175
+ </>
176
+
177
+ {/* Keywords */}
178
+ <>
179
+ <>
180
+ <Text
181
+ className="ml-1"
182
+ text="High-Level Keywords"
183
+ tooltip="List of high-level keywords to prioritize in retrieval. Separate with commas"
184
+ side="left"
185
+ />
186
+ <Input
187
+ id="hl_keywords"
188
+ type="text"
189
+ value={querySettings.hl_keywords?.join(', ')}
190
+ onChange={(e) => {
191
+ const keywords = e.target.value
192
+ .split(',')
193
+ .map((k) => k.trim())
194
+ .filter((k) => k !== '')
195
+ handleChange('hl_keywords', keywords)
196
+ }}
197
+ placeholder="Enter keywords"
198
+ />
199
+ </>
200
+
201
+ <>
202
+ <Text
203
+ className="ml-1"
204
+ text="Low-Level Keywords"
205
+ tooltip="List of low-level keywords to refine retrieval focus. Separate with commas"
206
+ side="left"
207
+ />
208
+ <Input
209
+ id="ll_keywords"
210
+ type="text"
211
+ value={querySettings.ll_keywords?.join(', ')}
212
+ onChange={(e) => {
213
+ const keywords = e.target.value
214
+ .split(',')
215
+ .map((k) => k.trim())
216
+ .filter((k) => k !== '')
217
+ handleChange('ll_keywords', keywords)
218
+ }}
219
+ placeholder="Enter keywords"
220
+ />
221
+ </>
222
+ </>
223
+
224
+ {/* Toggle Options */}
225
+ <>
226
+ <div className="flex items-center gap-2">
227
+ <Text
228
+ className="ml-1"
229
+ text="Only Need Context"
230
+ tooltip="If True, only returns the retrieved context without generating a response"
231
+ side="left"
232
+ />
233
+ <div className="grow" />
234
+ <Checkbox
235
+ className="mr-1 cursor-pointer"
236
+ id="only_need_context"
237
+ checked={querySettings.only_need_context}
238
+ onCheckedChange={(checked) => handleChange('only_need_context', checked)}
239
+ />
240
+ </div>
241
+
242
+ <div className="flex items-center gap-2">
243
+ <Text
244
+ className="ml-1"
245
+ text="Only Need Prompt"
246
+ tooltip="If True, only returns the generated prompt without producing a response"
247
+ side="left"
248
+ />
249
+ <div className="grow" />
250
+ <Checkbox
251
+ className="mr-1 cursor-pointer"
252
+ id="only_need_prompt"
253
+ checked={querySettings.only_need_prompt}
254
+ onCheckedChange={(checked) => handleChange('only_need_prompt', checked)}
255
+ />
256
+ </div>
257
+
258
+ <div className="flex items-center gap-2">
259
+ <Text
260
+ className="ml-1"
261
+ text="Stream Response"
262
+ tooltip="If True, enables streaming output for real-time responses"
263
+ side="left"
264
+ />
265
+ <div className="grow" />
266
+ <Checkbox
267
+ className="mr-1 cursor-pointer"
268
+ id="stream"
269
+ checked={querySettings.stream}
270
+ onCheckedChange={(checked) => handleChange('stream', checked)}
271
+ />
272
+ </div>
273
+ </>
274
+ </div>
275
+ </div>
276
+ </CardContent>
277
+ </Card>
278
+ )
279
+ }
lightrag_webui/src/components/ui/Checkbox.tsx CHANGED
@@ -23,4 +23,4 @@ const Checkbox = React.forwardRef<
23
  ))
24
  Checkbox.displayName = CheckboxPrimitive.Root.displayName
25
 
26
- export { Checkbox }
 
23
  ))
24
  Checkbox.displayName = CheckboxPrimitive.Root.displayName
25
 
26
+ export default Checkbox
lightrag_webui/src/components/ui/NumberInput.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChevronDown, ChevronUp } from 'lucide-react'
2
+ import { forwardRef, useCallback, useEffect, useState } from 'react'
3
+ import { NumericFormat, NumericFormatProps } from 'react-number-format'
4
+ import Button from '@/components/ui/Button'
5
+ import Input from '@/components/ui/Input'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ export interface NumberInputProps extends Omit<NumericFormatProps, 'value' | 'onValueChange'> {
9
+ stepper?: number
10
+ thousandSeparator?: string
11
+ placeholder?: string
12
+ defaultValue?: number
13
+ min?: number
14
+ max?: number
15
+ value?: number // Controlled value
16
+ suffix?: string
17
+ prefix?: string
18
+ onValueChange?: (value: number | undefined) => void
19
+ fixedDecimalScale?: boolean
20
+ decimalScale?: number
21
+ }
22
+
23
+ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
24
+ (
25
+ {
26
+ stepper,
27
+ thousandSeparator,
28
+ placeholder,
29
+ defaultValue,
30
+ min = -Infinity,
31
+ max = Infinity,
32
+ onValueChange,
33
+ fixedDecimalScale = false,
34
+ decimalScale = 0,
35
+ className = undefined,
36
+ suffix,
37
+ prefix,
38
+ value: controlledValue,
39
+ ...props
40
+ },
41
+ ref
42
+ ) => {
43
+ const [value, setValue] = useState<number | undefined>(controlledValue ?? defaultValue)
44
+
45
+ const handleIncrement = useCallback(() => {
46
+ setValue((prev) =>
47
+ prev === undefined ? (stepper ?? 1) : Math.min(prev + (stepper ?? 1), max)
48
+ )
49
+ }, [stepper, max])
50
+
51
+ const handleDecrement = useCallback(() => {
52
+ setValue((prev) =>
53
+ prev === undefined ? -(stepper ?? 1) : Math.max(prev - (stepper ?? 1), min)
54
+ )
55
+ }, [stepper, min])
56
+
57
+ useEffect(() => {
58
+ if (controlledValue !== undefined) {
59
+ setValue(controlledValue)
60
+ }
61
+ }, [controlledValue])
62
+
63
+ const handleChange = (values: { value: string; floatValue: number | undefined }) => {
64
+ const newValue = values.floatValue === undefined ? undefined : values.floatValue
65
+ setValue(newValue)
66
+ if (onValueChange) {
67
+ onValueChange(newValue)
68
+ }
69
+ }
70
+
71
+ const handleBlur = () => {
72
+ if (value !== undefined) {
73
+ if (value < min) {
74
+ setValue(min)
75
+ ;(ref as React.RefObject<HTMLInputElement>).current!.value = String(min)
76
+ } else if (value > max) {
77
+ setValue(max)
78
+ ;(ref as React.RefObject<HTMLInputElement>).current!.value = String(max)
79
+ }
80
+ }
81
+ }
82
+
83
+ return (
84
+ <div className="relative flex">
85
+ <NumericFormat
86
+ value={value}
87
+ onValueChange={handleChange}
88
+ thousandSeparator={thousandSeparator}
89
+ decimalScale={decimalScale}
90
+ fixedDecimalScale={fixedDecimalScale}
91
+ allowNegative={min < 0}
92
+ valueIsNumericString
93
+ onBlur={handleBlur}
94
+ max={max}
95
+ min={min}
96
+ suffix={suffix}
97
+ prefix={prefix}
98
+ customInput={(props) => <Input {...props} className={cn('w-full', className)} />}
99
+ placeholder={placeholder}
100
+ className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
101
+ getInputRef={ref}
102
+ {...props}
103
+ />
104
+ <div className="absolute top-0 right-0 bottom-0 flex flex-col">
105
+ <Button
106
+ aria-label="Increase value"
107
+ className="border-input h-1/2 rounded-l-none rounded-br-none border-b border-l px-2 focus-visible:relative"
108
+ variant="outline"
109
+ onClick={handleIncrement}
110
+ disabled={value === max}
111
+ >
112
+ <ChevronUp size={15} />
113
+ </Button>
114
+ <Button
115
+ aria-label="Decrease value"
116
+ className="border-input h-1/2 rounded-l-none rounded-tr-none border-b border-l px-2 focus-visible:relative"
117
+ variant="outline"
118
+ onClick={handleDecrement}
119
+ disabled={value === min}
120
+ >
121
+ <ChevronDown size={15} />
122
+ </Button>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+ )
128
+
129
+ NumberInput.displayName = 'NumberInput'
130
+
131
+ export default NumberInput
lightrag_webui/src/components/ui/Select.tsx ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import * as SelectPrimitive from '@radix-ui/react-select'
3
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
4
+
5
+ import { cn } from '@/lib/utils'
6
+
7
+ const Select = SelectPrimitive.Root
8
+
9
+ const SelectGroup = SelectPrimitive.Group
10
+
11
+ const SelectValue = SelectPrimitive.Value
12
+
13
+ const SelectTrigger = React.forwardRef<
14
+ React.ComponentRef<typeof SelectPrimitive.Trigger>,
15
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
16
+ >(({ className, children, ...props }, ref) => (
17
+ <SelectPrimitive.Trigger
18
+ ref={ref}
19
+ className={cn(
20
+ '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',
21
+ className
22
+ )}
23
+ {...props}
24
+ >
25
+ {children}
26
+ <SelectPrimitive.Icon asChild>
27
+ <ChevronDown className="h-4 w-4 opacity-50" />
28
+ </SelectPrimitive.Icon>
29
+ </SelectPrimitive.Trigger>
30
+ ))
31
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
32
+
33
+ const SelectScrollUpButton = React.forwardRef<
34
+ React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
35
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
36
+ >(({ className, ...props }, ref) => (
37
+ <SelectPrimitive.ScrollUpButton
38
+ ref={ref}
39
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
40
+ {...props}
41
+ >
42
+ <ChevronUp className="h-4 w-4" />
43
+ </SelectPrimitive.ScrollUpButton>
44
+ ))
45
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
46
+
47
+ const SelectScrollDownButton = React.forwardRef<
48
+ React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
49
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
50
+ >(({ className, ...props }, ref) => (
51
+ <SelectPrimitive.ScrollDownButton
52
+ ref={ref}
53
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
54
+ {...props}
55
+ >
56
+ <ChevronDown className="h-4 w-4" />
57
+ </SelectPrimitive.ScrollDownButton>
58
+ ))
59
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
60
+
61
+ const SelectContent = React.forwardRef<
62
+ React.ComponentRef<typeof SelectPrimitive.Content>,
63
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
64
+ >(({ className, children, position = 'popper', ...props }, ref) => (
65
+ <SelectPrimitive.Portal>
66
+ <SelectPrimitive.Content
67
+ ref={ref}
68
+ className={cn(
69
+ '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',
70
+ position === 'popper' &&
71
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
72
+ className
73
+ )}
74
+ position={position}
75
+ {...props}
76
+ >
77
+ <SelectScrollUpButton />
78
+ <SelectPrimitive.Viewport
79
+ className={cn(
80
+ 'p-1',
81
+ position === 'popper' &&
82
+ 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
83
+ )}
84
+ >
85
+ {children}
86
+ </SelectPrimitive.Viewport>
87
+ <SelectScrollDownButton />
88
+ </SelectPrimitive.Content>
89
+ </SelectPrimitive.Portal>
90
+ ))
91
+ SelectContent.displayName = SelectPrimitive.Content.displayName
92
+
93
+ const SelectLabel = React.forwardRef<
94
+ React.ComponentRef<typeof SelectPrimitive.Label>,
95
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
96
+ >(({ className, ...props }, ref) => (
97
+ <SelectPrimitive.Label
98
+ ref={ref}
99
+ className={cn('py-1.5 pr-2 pl-8 text-sm font-semibold', className)}
100
+ {...props}
101
+ />
102
+ ))
103
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
104
+
105
+ const SelectItem = React.forwardRef<
106
+ React.ComponentRef<typeof SelectPrimitive.Item>,
107
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
108
+ >(({ className, children, ...props }, ref) => (
109
+ <SelectPrimitive.Item
110
+ ref={ref}
111
+ className={cn(
112
+ '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',
113
+ className
114
+ )}
115
+ {...props}
116
+ >
117
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
118
+ <SelectPrimitive.ItemIndicator>
119
+ <Check className="h-4 w-4" />
120
+ </SelectPrimitive.ItemIndicator>
121
+ </span>
122
+
123
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
124
+ </SelectPrimitive.Item>
125
+ ))
126
+ SelectItem.displayName = SelectPrimitive.Item.displayName
127
+
128
+ const SelectSeparator = React.forwardRef<
129
+ React.ComponentRef<typeof SelectPrimitive.Separator>,
130
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
131
+ >(({ className, ...props }, ref) => (
132
+ <SelectPrimitive.Separator
133
+ ref={ref}
134
+ className={cn('bg-muted -mx-1 my-1 h-px', className)}
135
+ {...props}
136
+ />
137
+ ))
138
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
139
+
140
+ export {
141
+ Select,
142
+ SelectGroup,
143
+ SelectValue,
144
+ SelectTrigger,
145
+ SelectContent,
146
+ SelectLabel,
147
+ SelectItem,
148
+ SelectSeparator,
149
+ SelectScrollUpButton,
150
+ SelectScrollDownButton
151
+ }
lightrag_webui/src/components/ui/Tooltip.tsx CHANGED
@@ -8,19 +8,31 @@ const Tooltip = TooltipPrimitive.Root
8
 
9
  const TooltipTrigger = TooltipPrimitive.Trigger
10
 
 
 
 
 
 
 
 
 
 
 
11
  const TooltipContent = React.forwardRef<
12
  React.ComponentRef<typeof TooltipPrimitive.Content>,
13
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
14
- >(({ className, sideOffset = 4, ...props }, ref) => (
15
  <TooltipPrimitive.Content
16
  ref={ref}
17
  sideOffset={sideOffset}
18
  className={cn(
19
- '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',
20
  className
21
  )}
22
  {...props}
23
- />
 
 
24
  ))
25
  TooltipContent.displayName = TooltipPrimitive.Content.displayName
26
 
 
8
 
9
  const TooltipTrigger = TooltipPrimitive.Trigger
10
 
11
+ const processTooltipContent = (content: string) => {
12
+ if (typeof content !== 'string') return content
13
+ return content.split('\\n').map((line, i) => (
14
+ <React.Fragment key={i}>
15
+ {line}
16
+ {i < content.split('\\n').length - 1 && <br />}
17
+ </React.Fragment>
18
+ ))
19
+ }
20
+
21
  const TooltipContent = React.forwardRef<
22
  React.ComponentRef<typeof TooltipPrimitive.Content>,
23
  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
24
+ >(({ className, sideOffset = 4, children, ...props }, ref) => (
25
  <TooltipPrimitive.Content
26
  ref={ref}
27
  sideOffset={sideOffset}
28
  className={cn(
29
+ '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',
30
  className
31
  )}
32
  {...props}
33
+ >
34
+ {typeof children === 'string' ? processTooltipContent(children) : children}
35
+ </TooltipPrimitive.Content>
36
  ))
37
  TooltipContent.displayName = TooltipPrimitive.Content.displayName
38
 
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -11,8 +11,8 @@ import {
11
  import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
12
  import Progress from '@/components/ui/Progress'
13
  import EmptyCard from '@/components/ui/EmptyCard'
14
- import UploadDocumentsDialog from '@/components/document/UploadDocumentsDialog'
15
- import ClearDocumentsDialog from '@/components/document/ClearDocumentsDialog'
16
 
17
  import {
18
  getDocuments,
 
11
  import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
12
  import Progress from '@/components/ui/Progress'
13
  import EmptyCard from '@/components/ui/EmptyCard'
14
+ import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
15
+ import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
16
 
17
  import {
18
  getDocuments,
lightrag_webui/src/features/RetrievalTesting.tsx CHANGED
@@ -1,17 +1,13 @@
1
  import Input from '@/components/ui/Input'
2
  import Button from '@/components/ui/Button'
3
  import { useCallback, useEffect, useRef, useState } from 'react'
4
- import { queryTextStream, QueryMode } from '@/api/lightrag'
5
  import { errorMessage } from '@/lib/utils'
6
  import { useSettingsStore } from '@/stores/settings'
7
  import { useDebounce } from '@/hooks/useDebounce'
8
- import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react'
9
 
10
- type Message = {
11
- id: string
12
- content: string
13
- role: 'User' | 'LightRAG'
14
- }
15
 
16
  export default function RetrievalTesting() {
17
  const [messages, setMessages] = useState<Message[]>(
@@ -19,7 +15,6 @@ export default function RetrievalTesting() {
19
  )
20
  const [inputValue, setInputValue] = useState('')
21
  const [isLoading, setIsLoading] = useState(false)
22
- const [mode, setMode] = useState<QueryMode>('mix')
23
  const messagesEndRef = useRef<HTMLDivElement>(null)
24
 
25
  const scrollToBottom = useCallback(() => {
@@ -31,23 +26,24 @@ export default function RetrievalTesting() {
31
  e.preventDefault()
32
  if (!inputValue.trim() || isLoading) return
33
 
 
34
  const userMessage: Message = {
35
- id: Date.now().toString(),
36
  content: inputValue,
37
- role: 'User'
38
  }
39
 
40
  const assistantMessage: Message = {
41
- id: (Date.now() + 1).toString(),
42
  content: '',
43
- role: 'LightRAG'
44
  }
45
 
 
46
  setMessages((prev) => {
47
  const newMessages = [...prev, userMessage, assistantMessage]
48
  return newMessages
49
  })
50
 
 
51
  setInputValue('')
52
  setIsLoading(true)
53
 
@@ -57,25 +53,34 @@ export default function RetrievalTesting() {
57
  setMessages((prev) => {
58
  const newMessages = [...prev]
59
  const lastMessage = newMessages[newMessages.length - 1]
60
- if (lastMessage.role === 'LightRAG') {
61
  lastMessage.content = assistantMessage.content
62
  }
63
  return newMessages
64
  })
65
  }
66
 
 
 
 
 
 
 
 
 
67
  try {
68
- await queryTextStream(
69
- {
70
- query: userMessage.content,
71
- mode: mode,
72
- stream: true
73
- },
74
- updateAssistantMessage
75
- )
76
  } catch (err) {
 
77
  updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`)
78
  } finally {
 
79
  setIsLoading(false)
80
  useSettingsStore
81
  .getState()
@@ -86,7 +91,7 @@ export default function RetrievalTesting() {
86
  ])
87
  }
88
  },
89
- [inputValue, isLoading, mode, setMessages]
90
  )
91
 
92
  const debouncedMessages = useDebounce(messages, 100)
@@ -98,73 +103,65 @@ export default function RetrievalTesting() {
98
  }, [setMessages])
99
 
100
  return (
101
- <div className="flex size-full flex-col gap-4 px-32 py-6">
102
- <div className="relative grow">
103
- <div className="bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2">
104
- <div className="flex min-h-0 flex-1 flex-col gap-2">
105
- {messages.length === 0 ? (
106
- <div className="text-muted-foreground flex h-full items-center justify-center text-lg">
107
- Start a retrieval by typing your query below
108
- </div>
109
- ) : (
110
- messages.map((message) => (
111
- <div
112
- key={message.id}
113
- className={`flex ${message.role === 'User' ? 'justify-end' : 'justify-start'}`}
114
- >
115
  <div
116
- className={`max-w-[80%] rounded-lg px-4 py-2 ${
117
- message.role === 'User' ? 'bg-primary text-primary-foreground' : 'bg-muted'
118
- }`}
119
  >
120
- <pre className="break-words whitespace-pre-wrap">{message.content}</pre>
121
- {message.content.length === 0 && (
122
- <LoaderIcon className="animate-spin duration-2000" />
123
- )}
 
 
 
 
 
 
124
  </div>
125
- </div>
126
- ))
127
- )}
128
- <div ref={messagesEndRef} className="pb-1" />
129
  </div>
130
  </div>
131
- </div>
132
 
133
- <form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2 pb-2">
134
- <Button
135
- type="button"
136
- variant="outline"
137
- onClick={clearMessages}
138
- disabled={isLoading}
139
- size="sm"
140
- >
141
- <EraserIcon />
142
- Clear
143
- </Button>
144
- <select
145
- className="border-input bg-background ring-offset-background h-9 rounded-md border px-3 py-1 text-sm"
146
- value={mode}
147
- onChange={(e) => setMode(e.target.value as QueryMode)}
148
- disabled={isLoading}
149
- >
150
- <option value="naive">Naive</option>
151
- <option value="local">Local</option>
152
- <option value="global">Global</option>
153
- <option value="hybrid">Hybrid</option>
154
- <option value="mix">Mix</option>
155
- </select>
156
- <Input
157
- className="flex-1"
158
- value={inputValue}
159
- onChange={(e) => setInputValue(e.target.value)}
160
- placeholder="Type your query..."
161
- disabled={isLoading}
162
- />
163
- <Button type="submit" variant="default" disabled={isLoading} size="sm">
164
- <SendIcon />
165
- Send
166
- </Button>
167
- </form>
168
  </div>
169
  )
170
  }
 
 
1
  import Input from '@/components/ui/Input'
2
  import Button from '@/components/ui/Button'
3
  import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import { queryText, queryTextStream, Message } from '@/api/lightrag'
5
  import { errorMessage } from '@/lib/utils'
6
  import { useSettingsStore } from '@/stores/settings'
7
  import { useDebounce } from '@/hooks/useDebounce'
8
+ import QuerySettings from '@/components/retrieval/QuerySettings'
9
 
10
+ import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react'
 
 
 
 
11
 
12
  export default function RetrievalTesting() {
13
  const [messages, setMessages] = useState<Message[]>(
 
15
  )
16
  const [inputValue, setInputValue] = useState('')
17
  const [isLoading, setIsLoading] = useState(false)
 
18
  const messagesEndRef = useRef<HTMLDivElement>(null)
19
 
20
  const scrollToBottom = useCallback(() => {
 
26
  e.preventDefault()
27
  if (!inputValue.trim() || isLoading) return
28
 
29
+ // Create messages
30
  const userMessage: Message = {
 
31
  content: inputValue,
32
+ role: 'user'
33
  }
34
 
35
  const assistantMessage: Message = {
 
36
  content: '',
37
+ role: 'assistant'
38
  }
39
 
40
+ // Add messages to chatbox
41
  setMessages((prev) => {
42
  const newMessages = [...prev, userMessage, assistantMessage]
43
  return newMessages
44
  })
45
 
46
+ // Clear input and set loading
47
  setInputValue('')
48
  setIsLoading(true)
49
 
 
53
  setMessages((prev) => {
54
  const newMessages = [...prev]
55
  const lastMessage = newMessages[newMessages.length - 1]
56
+ if (lastMessage.role === 'assistant') {
57
  lastMessage.content = assistantMessage.content
58
  }
59
  return newMessages
60
  })
61
  }
62
 
63
+ // Prepare query parameters
64
+ const state = useSettingsStore.getState()
65
+ const queryParams = {
66
+ ...state.querySettings,
67
+ query: userMessage.content,
68
+ conversation_history: messages
69
+ }
70
+
71
  try {
72
+ // Run query
73
+ if (state.querySettings.stream) {
74
+ await queryTextStream(queryParams, updateAssistantMessage)
75
+ } else {
76
+ const response = await queryText(queryParams)
77
+ updateAssistantMessage(response.response)
78
+ }
 
79
  } catch (err) {
80
+ // Handle error
81
  updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`)
82
  } finally {
83
+ // Clear loading and add messages to state
84
  setIsLoading(false)
85
  useSettingsStore
86
  .getState()
 
91
  ])
92
  }
93
  },
94
+ [inputValue, isLoading, messages, setMessages]
95
  )
96
 
97
  const debouncedMessages = useDebounce(messages, 100)
 
103
  }, [setMessages])
104
 
105
  return (
106
+ <div className="flex size-full gap-2 px-2 pb-12">
107
+ <div className="flex grow flex-col gap-4">
108
+ <div className="relative grow">
109
+ <div className="bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2">
110
+ <div className="flex min-h-0 flex-1 flex-col gap-2">
111
+ {messages.length === 0 ? (
112
+ <div className="text-muted-foreground flex h-full items-center justify-center text-lg">
113
+ Start a retrieval by typing your query below
114
+ </div>
115
+ ) : (
116
+ messages.map((message, idx) => (
 
 
 
117
  <div
118
+ key={idx}
119
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
 
120
  >
121
+ <div
122
+ className={`max-w-[80%] rounded-lg px-4 py-2 ${
123
+ message.role === 'user' ? 'bg-primary text-primary-foreground' : 'bg-muted'
124
+ }`}
125
+ >
126
+ <pre className="break-words whitespace-pre-wrap">{message.content}</pre>
127
+ {message.content.length === 0 && (
128
+ <LoaderIcon className="animate-spin duration-2000" />
129
+ )}
130
+ </div>
131
  </div>
132
+ ))
133
+ )}
134
+ <div ref={messagesEndRef} className="pb-1" />
135
+ </div>
136
  </div>
137
  </div>
 
138
 
139
+ <form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2">
140
+ <Button
141
+ type="button"
142
+ variant="outline"
143
+ onClick={clearMessages}
144
+ disabled={isLoading}
145
+ size="sm"
146
+ >
147
+ <EraserIcon />
148
+ Clear
149
+ </Button>
150
+ <Input
151
+ className="flex-1"
152
+ value={inputValue}
153
+ onChange={(e) => setInputValue(e.target.value)}
154
+ placeholder="Type your query..."
155
+ disabled={isLoading}
156
+ />
157
+ <Button type="submit" variant="default" disabled={isLoading} size="sm">
158
+ <SendIcon />
159
+ Send
160
+ </Button>
161
+ </form>
162
+ </div>
163
+ <QuerySettings />
 
 
 
 
 
 
 
 
 
 
164
  </div>
165
  )
166
  }
167
+
lightrag_webui/src/stores/settings.ts CHANGED
@@ -2,6 +2,7 @@ import { create } from 'zustand'
2
  import { persist, createJSONStorage } from 'zustand/middleware'
3
  import { createSelectors } from '@/lib/utils'
4
  import { defaultQueryLabel } from '@/lib/constants'
 
5
 
6
  type Theme = 'dark' | 'light' | 'system'
7
  type Tab = 'documents' | 'knowledge-graph' | 'retrieval'
@@ -32,8 +33,11 @@ interface SettingsState {
32
  currentTab: Tab
33
  setCurrentTab: (tab: Tab) => void
34
 
35
- retrievalHistory: any[]
36
- setRetrievalHistory: (history: any[]) => void
 
 
 
37
  }
38
 
39
  const useSettingsStoreBase = create<SettingsState>()(
@@ -60,6 +64,21 @@ const useSettingsStoreBase = create<SettingsState>()(
60
 
61
  retrievalHistory: [],
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  setTheme: (theme: Theme) => set({ theme }),
64
 
65
  setQueryLabel: (queryLabel: string) =>
@@ -73,15 +92,17 @@ const useSettingsStoreBase = create<SettingsState>()(
73
 
74
  setCurrentTab: (tab: Tab) => set({ currentTab: tab }),
75
 
76
- setRetrievalHistory: (history: any[]) => set({ retrievalHistory: history })
 
 
 
 
 
77
  }),
78
  {
79
  name: 'settings-storage',
80
  storage: createJSONStorage(() => localStorage),
81
- version: 5,
82
- partialize(state) {
83
- return { ...state, retrievalHistory: undefined }
84
- },
85
  migrate: (state: any, version: number) => {
86
  if (version < 2) {
87
  state.showEdgeLabel = false
@@ -99,6 +120,24 @@ const useSettingsStoreBase = create<SettingsState>()(
99
  if (version < 5) {
100
  state.currentTab = 'documents'
101
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
  }
104
  )
 
2
  import { persist, createJSONStorage } from 'zustand/middleware'
3
  import { createSelectors } from '@/lib/utils'
4
  import { defaultQueryLabel } from '@/lib/constants'
5
+ import { Message, QueryRequest } from '@/api/lightrag'
6
 
7
  type Theme = 'dark' | 'light' | 'system'
8
  type Tab = 'documents' | 'knowledge-graph' | 'retrieval'
 
33
  currentTab: Tab
34
  setCurrentTab: (tab: Tab) => void
35
 
36
+ retrievalHistory: Message[]
37
+ setRetrievalHistory: (history: Message[]) => void
38
+
39
+ querySettings: Omit<QueryRequest, 'query'>
40
+ updateQuerySettings: (settings: Partial<QueryRequest>) => void
41
  }
42
 
43
  const useSettingsStoreBase = create<SettingsState>()(
 
64
 
65
  retrievalHistory: [],
66
 
67
+ querySettings: {
68
+ mode: 'global',
69
+ response_type: 'Multiple Paragraphs',
70
+ top_k: 10,
71
+ max_token_for_text_unit: 4000,
72
+ max_token_for_global_context: 4000,
73
+ max_token_for_local_context: 4000,
74
+ only_need_context: false,
75
+ only_need_prompt: false,
76
+ stream: true,
77
+ history_turns: 3,
78
+ hl_keywords: [],
79
+ ll_keywords: []
80
+ },
81
+
82
  setTheme: (theme: Theme) => set({ theme }),
83
 
84
  setQueryLabel: (queryLabel: string) =>
 
92
 
93
  setCurrentTab: (tab: Tab) => set({ currentTab: tab }),
94
 
95
+ setRetrievalHistory: (history: Message[]) => set({ retrievalHistory: history }),
96
+
97
+ updateQuerySettings: (settings: Partial<QueryRequest>) =>
98
+ set((state) => ({
99
+ querySettings: { ...state.querySettings, ...settings }
100
+ }))
101
  }),
102
  {
103
  name: 'settings-storage',
104
  storage: createJSONStorage(() => localStorage),
105
+ version: 6,
 
 
 
106
  migrate: (state: any, version: number) => {
107
  if (version < 2) {
108
  state.showEdgeLabel = false
 
120
  if (version < 5) {
121
  state.currentTab = 'documents'
122
  }
123
+ if (version < 6) {
124
+ state.querySettings = {
125
+ mode: 'global',
126
+ response_type: 'Multiple Paragraphs',
127
+ top_k: 10,
128
+ max_token_for_text_unit: 4000,
129
+ max_token_for_global_context: 4000,
130
+ max_token_for_local_context: 4000,
131
+ only_need_context: false,
132
+ only_need_prompt: false,
133
+ stream: true,
134
+ history_turns: 3,
135
+ hl_keywords: [],
136
+ ll_keywords: []
137
+ }
138
+ state.retrievalHistory = []
139
+ }
140
+ return state
141
  }
142
  }
143
  )