ArnoChen commited on
Commit
3578090
·
1 Parent(s): d42b23a

add label filter

Browse files
lightrag/api/graph_viewer_webui/bun.lock CHANGED
@@ -27,6 +27,7 @@
27
  "graphology": "^0.26.0",
28
  "graphology-generators": "^0.11.2",
29
  "lucide-react": "^0.475.0",
 
30
  "react": "^19.0.0",
31
  "react-dom": "^19.0.0",
32
  "seedrandom": "^3.0.5",
 
27
  "graphology": "^0.26.0",
28
  "graphology-generators": "^0.11.2",
29
  "lucide-react": "^0.475.0",
30
+ "minisearch": "^7.1.1",
31
  "react": "^19.0.0",
32
  "react-dom": "^19.0.0",
33
  "seedrandom": "^3.0.5",
lightrag/api/graph_viewer_webui/package.json CHANGED
@@ -33,6 +33,7 @@
33
  "graphology": "^0.26.0",
34
  "graphology-generators": "^0.11.2",
35
  "lucide-react": "^0.475.0",
 
36
  "react": "^19.0.0",
37
  "react-dom": "^19.0.0",
38
  "seedrandom": "^3.0.5",
 
33
  "graphology": "^0.26.0",
34
  "graphology-generators": "^0.11.2",
35
  "lucide-react": "^0.475.0",
36
+ "minisearch": "^7.1.1",
37
  "react": "^19.0.0",
38
  "react-dom": "^19.0.0",
39
  "seedrandom": "^3.0.5",
lightrag/api/graph_viewer_webui/src/App.tsx CHANGED
@@ -1,18 +1,28 @@
1
  import ThemeProvider from '@/components/ThemeProvider'
2
- import BackendMessageAlert from '@/components/BackendMessageAlert'
3
  import { GraphViewer } from '@/GraphViewer'
4
  import { cn } from '@/lib/utils'
 
5
  import { useBackendState } from '@/stores/state'
 
6
 
7
  function App() {
8
- const health = useBackendState.use.health()
 
 
 
 
 
 
 
 
9
 
10
  return (
11
  <ThemeProvider>
12
- <div className={cn('h-screen w-screen', !health && 'pointer-events-none')}>
13
  <GraphViewer />
14
  </div>
15
- {!health && <BackendMessageAlert />}
16
  </ThemeProvider>
17
  )
18
  }
 
1
  import ThemeProvider from '@/components/ThemeProvider'
2
+ import MessageAlert from '@/components/MessageAlert'
3
  import { GraphViewer } from '@/GraphViewer'
4
  import { cn } from '@/lib/utils'
5
+ import { healthCheckInterval } from '@/lib/constants'
6
  import { useBackendState } from '@/stores/state'
7
+ import { useEffect } from 'react'
8
 
9
  function App() {
10
+ const message = useBackendState.use.message()
11
+
12
+ // health check
13
+ useEffect(() => {
14
+ const interval = setInterval(async () => {
15
+ await useBackendState.getState().check()
16
+ }, healthCheckInterval * 1000)
17
+ return () => clearInterval(interval)
18
+ }, [])
19
 
20
  return (
21
  <ThemeProvider>
22
+ <div className={cn('h-screen w-screen', message !== null && 'pointer-events-none')}>
23
  <GraphViewer />
24
  </div>
25
+ {message !== null && <MessageAlert />}
26
  </ThemeProvider>
27
  )
28
  }
lightrag/api/graph_viewer_webui/src/GraphViewer.tsx CHANGED
@@ -15,6 +15,7 @@ import ZoomControl from '@/components/ZoomControl'
15
  import FullScreenControl from '@/components/FullScreenControl'
16
  import Settings from '@/components/Settings'
17
  import GraphSearch from '@/components/GraphSearch'
 
18
  import PropertiesView from '@/components/PropertiesView'
19
 
20
  import { useSettingsStore } from '@/stores/settings'
@@ -144,9 +145,9 @@ export const GraphViewer = () => {
144
 
145
  <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
146
 
147
- <div className="absolute top-2 left-2">
 
148
  <GraphSearch
149
- type="nodes"
150
  value={searchInitSelectedNode}
151
  onFocus={onSearchFocus}
152
  onChange={onSearchSelect}
 
15
  import FullScreenControl from '@/components/FullScreenControl'
16
  import Settings from '@/components/Settings'
17
  import GraphSearch from '@/components/GraphSearch'
18
+ import GraphLabels from '@/components/GraphLabels'
19
  import PropertiesView from '@/components/PropertiesView'
20
 
21
  import { useSettingsStore } from '@/stores/settings'
 
145
 
146
  <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
147
 
148
+ <div className="absolute top-2 left-2 flex items-start gap-2">
149
+ <GraphLabels />
150
  <GraphSearch
 
151
  value={searchInitSelectedNode}
152
  onFocus={onSearchFocus}
153
  onChange={onSearchSelect}
lightrag/api/graph_viewer_webui/src/api/lightrag.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { backendBaseUrl } from '@/lib/constants'
 
2
 
3
  export type LightragNodeType = {
4
  id: string
@@ -81,7 +82,7 @@ export const checkHealth = async (): Promise<
81
  } catch (e) {
82
  return {
83
  status: 'error',
84
- message: e instanceof Error ? e.message : `${e}`
85
  }
86
  }
87
  }
 
1
  import { backendBaseUrl } from '@/lib/constants'
2
+ import { errorMessage } from '@/lib/utils'
3
 
4
  export type LightragNodeType = {
5
  id: string
 
82
  } catch (e) {
83
  return {
84
  status: 'error',
85
+ message: errorMessage(e)
86
  }
87
  }
88
  }
lightrag/api/graph_viewer_webui/src/components/GraphLabels.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useState } from 'react'
2
+ import { AsyncSelect } from '@/components/ui/AsyncSelect'
3
+ import { getGraphLabels } from '@/api/lightrag'
4
+ import { useSettingsStore } from '@/stores/settings'
5
+ import MiniSearch from 'minisearch'
6
+
7
+ const GraphLabels = () => {
8
+ const label = useSettingsStore.use.queryLabel()
9
+ const [labels, setLabels] = useState<{
10
+ labels: string[]
11
+ searchEngine: MiniSearch | null
12
+ }>({
13
+ labels: [],
14
+ searchEngine: null
15
+ })
16
+ const [fetched, setFetched] = useState(false)
17
+
18
+ const fetchData = useCallback(
19
+ async (query?: string): Promise<string[]> => {
20
+ let _labels = labels.labels
21
+ let _searchEngine = labels.searchEngine
22
+
23
+ if (!fetched || !_searchEngine) {
24
+ _labels = ['*'].concat(await getGraphLabels())
25
+
26
+ // Ensure query label exists
27
+ if (!_labels.includes(useSettingsStore.getState().queryLabel)) {
28
+ useSettingsStore.getState().setQueryLabel(_labels[0])
29
+ }
30
+
31
+ // Create search engine
32
+ _searchEngine = new MiniSearch({
33
+ idField: 'id',
34
+ fields: ['value'],
35
+ searchOptions: {
36
+ prefix: true,
37
+ fuzzy: 0.2,
38
+ boost: {
39
+ label: 2
40
+ }
41
+ }
42
+ })
43
+
44
+ // Add documents
45
+ const documents = _labels.map((str, index) => ({ id: index, value: str }))
46
+ _searchEngine.addAll(documents)
47
+
48
+ setLabels({
49
+ labels: _labels,
50
+ searchEngine: _searchEngine
51
+ })
52
+ setFetched(true)
53
+ }
54
+ if (!query) {
55
+ return _labels
56
+ }
57
+
58
+ // Search labels
59
+ return _searchEngine.search(query).map((result) => _labels[result.id])
60
+ },
61
+ [labels, fetched, setLabels, setFetched]
62
+ )
63
+
64
+ const setQueryLabel = useCallback((label: string) => {
65
+ useSettingsStore.getState().setQueryLabel(label)
66
+ }, [])
67
+
68
+ return (
69
+ <AsyncSelect<string>
70
+ className="ml-2"
71
+ triggerClassName="max-h-8"
72
+ searchInputClassName="max-h-8"
73
+ triggerTooltip="Select query label"
74
+ fetcher={fetchData}
75
+ renderOption={(item) => <div>{item}</div>}
76
+ getOptionValue={(item) => item}
77
+ getDisplayValue={(item) => <div>{item}</div>}
78
+ notFound={<div className="py-6 text-center text-sm">No labels found</div>}
79
+ label="Label"
80
+ placeholder="Search labels..."
81
+ value={label !== null ? label : ''}
82
+ onChange={setQueryLabel}
83
+ />
84
+ )
85
+ }
86
+
87
+ export default GraphLabels
lightrag/api/graph_viewer_webui/src/components/GraphSearch.tsx CHANGED
@@ -1,14 +1,14 @@
1
- import { FC, useCallback } from 'react'
2
  import {
3
  EdgeById,
4
  NodeById,
5
- useGraphSearch,
6
  GraphSearchInputProps,
7
- GraphSearchContextProvider,
8
  GraphSearchContextProviderProps
9
  } from '@react-sigma/graph-search'
10
- import { AsyncSelect } from '@/components/ui/AsyncSelect'
11
  import { searchResultLimit } from '@/lib/constants'
 
 
12
 
13
  interface OptionItem {
14
  id: string
@@ -27,6 +27,10 @@ function OptionComponent(item: OptionItem) {
27
  }
28
 
29
  const messageId = '__message_item'
 
 
 
 
30
 
31
  /**
32
  * Component thats display the search input.
@@ -34,15 +38,44 @@ const messageId = '__message_item'
34
  export const GraphSearchInput = ({
35
  onChange,
36
  onFocus,
37
- type,
38
  value
39
  }: {
40
  onChange: GraphSearchInputProps['onChange']
41
  onFocus?: GraphSearchInputProps['onFocus']
42
- type?: GraphSearchInputProps['type']
43
  value?: GraphSearchInputProps['value']
44
  }) => {
45
- const { search } = useGraphSearch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  /**
48
  * Loading the options while the user is typing.
@@ -50,8 +83,11 @@ export const GraphSearchInput = ({
50
  const loadOptions = useCallback(
51
  async (query?: string): Promise<OptionItem[]> => {
52
  if (onFocus) onFocus(null)
53
- if (!query) return []
54
- const result = (await search(query, type)) as OptionItem[]
 
 
 
55
 
56
  // prettier-ignore
57
  return result.length <= searchResultLimit
@@ -65,25 +101,24 @@ export const GraphSearchInput = ({
65
  }
66
  ]
67
  },
68
- [type, search, onFocus]
69
  )
70
 
71
  return (
72
- <AsyncSelect
73
- className="bg-background/60 w-52 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-opacity hover:opacity-100"
74
  fetcher={loadOptions}
75
  renderOption={OptionComponent}
76
  getOptionValue={(item) => item.id}
77
  value={value && value.type !== 'message' ? value.id : null}
78
  onChange={(id) => {
79
- if (id !== messageId && type) onChange(id ? { id, type } : null)
80
  }}
81
  onFocus={(id) => {
82
- if (id !== messageId && onFocus && type) onFocus(id ? { id, type } : null)
83
  }}
84
  label={'item'}
85
- preload={false}
86
- placeholder="Type search here..."
87
  />
88
  )
89
  }
@@ -91,13 +126,8 @@ export const GraphSearchInput = ({
91
  /**
92
  * Component that display the search.
93
  */
94
- const GraphSearch: FC<GraphSearchInputProps & GraphSearchContextProviderProps> = ({
95
- minisearchOptions,
96
- ...props
97
- }) => (
98
- <GraphSearchContextProvider minisearchOptions={minisearchOptions}>
99
- <GraphSearchInput {...props} />
100
- </GraphSearchContextProvider>
101
- )
102
 
103
  export default GraphSearch
 
1
+ import { FC, useCallback, useMemo } from 'react'
2
  import {
3
  EdgeById,
4
  NodeById,
 
5
  GraphSearchInputProps,
 
6
  GraphSearchContextProviderProps
7
  } from '@react-sigma/graph-search'
8
+ import { AsyncSearch } from '@/components/ui/AsyncSearch'
9
  import { searchResultLimit } from '@/lib/constants'
10
+ import { useGraphStore } from '@/stores/graph'
11
+ import MiniSearch from 'minisearch'
12
 
13
  interface OptionItem {
14
  id: string
 
27
  }
28
 
29
  const messageId = '__message_item'
30
+ const lastGraph: any = {
31
+ graph: null,
32
+ searchEngine: null
33
+ }
34
 
35
  /**
36
  * Component thats display the search input.
 
38
  export const GraphSearchInput = ({
39
  onChange,
40
  onFocus,
 
41
  value
42
  }: {
43
  onChange: GraphSearchInputProps['onChange']
44
  onFocus?: GraphSearchInputProps['onFocus']
 
45
  value?: GraphSearchInputProps['value']
46
  }) => {
47
+ const graph = useGraphStore.use.sigmaGraph()
48
+
49
+ const search = useMemo(() => {
50
+ if (lastGraph.graph == graph) {
51
+ return lastGraph.searchEngine
52
+ }
53
+ if (!graph || graph.nodes().length == 0) return
54
+
55
+ lastGraph.graph = graph
56
+
57
+ const searchEngine = new MiniSearch({
58
+ idField: 'id',
59
+ fields: ['label'],
60
+ searchOptions: {
61
+ prefix: true,
62
+ fuzzy: 0.2,
63
+ boost: {
64
+ label: 2
65
+ }
66
+ }
67
+ })
68
+
69
+ // Add documents
70
+ const documents = graph.nodes().map((id: string) => ({
71
+ id: id,
72
+ label: graph.getNodeAttribute(id, 'label')
73
+ }))
74
+ searchEngine.addAll(documents)
75
+
76
+ lastGraph.searchEngine = searchEngine
77
+ return searchEngine
78
+ }, [graph])
79
 
80
  /**
81
  * Loading the options while the user is typing.
 
83
  const loadOptions = useCallback(
84
  async (query?: string): Promise<OptionItem[]> => {
85
  if (onFocus) onFocus(null)
86
+ if (!query || !search) return []
87
+ const result: OptionItem[] = search.search(query).map((result) => ({
88
+ id: result.id,
89
+ type: 'nodes'
90
+ }))
91
 
92
  // prettier-ignore
93
  return result.length <= searchResultLimit
 
101
  }
102
  ]
103
  },
104
+ [search, onFocus]
105
  )
106
 
107
  return (
108
+ <AsyncSearch
109
+ className="bg-background/60 w-24 rounded-xl border-1 opacity-60 backdrop-blur-lg transition-all hover:w-fit hover:opacity-100"
110
  fetcher={loadOptions}
111
  renderOption={OptionComponent}
112
  getOptionValue={(item) => item.id}
113
  value={value && value.type !== 'message' ? value.id : null}
114
  onChange={(id) => {
115
+ if (id !== messageId) onChange(id ? { id, type: 'nodes' } : null)
116
  }}
117
  onFocus={(id) => {
118
+ if (id !== messageId && onFocus) onFocus(id ? { id, type: 'nodes' } : null)
119
  }}
120
  label={'item'}
121
+ placeholder="Search nodes..."
 
122
  />
123
  )
124
  }
 
126
  /**
127
  * Component that display the search.
128
  */
129
+ const GraphSearch: FC<GraphSearchInputProps & GraphSearchContextProviderProps> = ({ ...props }) => {
130
+ return <GraphSearchInput {...props} />
131
+ }
 
 
 
 
 
132
 
133
  export default GraphSearch
lightrag/api/graph_viewer_webui/src/components/{BackendMessageAlert.tsx → MessageAlert.tsx} RENAMED
@@ -1,8 +1,11 @@
1
  import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
 
2
  import { useBackendState } from '@/stores/state'
 
 
3
  import { AlertCircle } from 'lucide-react'
4
 
5
- const BackendMessageAlert = () => {
6
  const health = useBackendState.use.health()
7
  const message = useBackendState.use.message()
8
  const messageTitle = useBackendState.use.messageTitle()
@@ -10,13 +13,26 @@ const BackendMessageAlert = () => {
10
  return (
11
  <Alert
12
  variant={health ? 'default' : 'destructive'}
13
- className="absolute top-1/2 left-1/2 w-auto -translate-x-1/2 -translate-y-1/2 transform"
14
  >
15
  {!health && <AlertCircle className="h-4 w-4" />}
16
  <AlertTitle>{messageTitle}</AlertTitle>
 
17
  <AlertDescription>{message}</AlertDescription>
 
 
 
 
 
 
 
 
 
 
 
 
18
  </Alert>
19
  )
20
  }
21
 
22
- export default BackendMessageAlert
 
1
  import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
2
+ import Button from '@/components/ui/Button'
3
  import { useBackendState } from '@/stores/state'
4
+ import { controlButtonVariant } from '@/lib/constants'
5
+
6
  import { AlertCircle } from 'lucide-react'
7
 
8
+ const MessageAlert = () => {
9
  const health = useBackendState.use.health()
10
  const message = useBackendState.use.message()
11
  const messageTitle = useBackendState.use.messageTitle()
 
13
  return (
14
  <Alert
15
  variant={health ? 'default' : 'destructive'}
16
+ className="bg-background/90 absolute top-1/2 left-1/2 w-auto -translate-x-1/2 -translate-y-1/2 transform backdrop-blur-lg"
17
  >
18
  {!health && <AlertCircle className="h-4 w-4" />}
19
  <AlertTitle>{messageTitle}</AlertTitle>
20
+
21
  <AlertDescription>{message}</AlertDescription>
22
+ <div className="h-2" />
23
+ <div className="flex">
24
+ <div className="flex-auto" />
25
+ <Button
26
+ size="sm"
27
+ variant={controlButtonVariant}
28
+ className="text-primary max-h-8 border !p-2 text-xs"
29
+ onClick={() => useBackendState.getState().clear()}
30
+ >
31
+ Continue
32
+ </Button>
33
+ </div>
34
  </Alert>
35
  )
36
  }
37
 
38
+ export default MessageAlert
lightrag/api/graph_viewer_webui/src/components/ui/AsyncSearch.tsx ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { Loader2 } from 'lucide-react'
3
+ import { useDebounce } from '@/hooks/useDebounce'
4
+
5
+ import { cn } from '@/lib/utils'
6
+ import {
7
+ Command,
8
+ CommandEmpty,
9
+ CommandGroup,
10
+ CommandInput,
11
+ CommandItem,
12
+ CommandList
13
+ } from '@/components/ui/Command'
14
+
15
+ export interface Option {
16
+ value: string
17
+ label: string
18
+ disabled?: boolean
19
+ description?: string
20
+ icon?: React.ReactNode
21
+ }
22
+
23
+ export interface AsyncSearchProps<T> {
24
+ /** Async function to fetch options */
25
+ fetcher: (query?: string) => Promise<T[]>
26
+ /** Preload all data ahead of time */
27
+ preload?: boolean
28
+ /** Function to filter options */
29
+ filterFn?: (option: T, query: string) => boolean
30
+ /** Function to render each option */
31
+ renderOption: (option: T) => React.ReactNode
32
+ /** Function to get the value from an option */
33
+ getOptionValue: (option: T) => string
34
+ /** Custom not found message */
35
+ notFound?: React.ReactNode
36
+ /** Custom loading skeleton */
37
+ loadingSkeleton?: React.ReactNode
38
+ /** Currently selected value */
39
+ value: string | null
40
+ /** Callback when selection changes */
41
+ onChange: (value: string) => void
42
+ /** Callback when focus changes */
43
+ onFocus: (value: string) => void
44
+ /** Label for the select field */
45
+ label: string
46
+ /** Placeholder text when no selection */
47
+ placeholder?: string
48
+ /** Disable the entire select */
49
+ disabled?: boolean
50
+ /** Custom width for the popover */
51
+ width?: string | number
52
+ /** Custom class names */
53
+ className?: string
54
+ /** Custom trigger button class names */
55
+ triggerClassName?: string
56
+ /** Custom no results message */
57
+ noResultsMessage?: string
58
+ /** Allow clearing the selection */
59
+ clearable?: boolean
60
+ }
61
+
62
+ export function AsyncSearch<T>({
63
+ fetcher,
64
+ preload,
65
+ filterFn,
66
+ renderOption,
67
+ getOptionValue,
68
+ notFound,
69
+ loadingSkeleton,
70
+ label,
71
+ placeholder = 'Select...',
72
+ value,
73
+ onChange,
74
+ onFocus,
75
+ disabled = false,
76
+ className,
77
+ noResultsMessage
78
+ }: AsyncSearchProps<T>) {
79
+ const [mounted, setMounted] = useState(false)
80
+ const [open, setOpen] = useState(false)
81
+ const [options, setOptions] = useState<T[]>([])
82
+ const [loading, setLoading] = useState(false)
83
+ const [error, setError] = useState<string | null>(null)
84
+ const [selectedValue, setSelectedValue] = useState(value)
85
+ const [focusedValue, setFocusedValue] = useState<string | null>(null)
86
+ const [searchTerm, setSearchTerm] = useState('')
87
+ const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
88
+ const [originalOptions, setOriginalOptions] = useState<T[]>([])
89
+
90
+ useEffect(() => {
91
+ setMounted(true)
92
+ setSelectedValue(value)
93
+ }, [value])
94
+
95
+ // Effect for initial fetch
96
+ useEffect(() => {
97
+ const initializeOptions = async () => {
98
+ try {
99
+ setLoading(true)
100
+ setError(null)
101
+ // If we have a value, use it for the initial search
102
+ const data = value !== null ? await fetcher(value) : []
103
+ setOriginalOptions(data)
104
+ setOptions(data)
105
+ } catch (err) {
106
+ setError(err instanceof Error ? err.message : 'Failed to fetch options')
107
+ } finally {
108
+ setLoading(false)
109
+ }
110
+ }
111
+
112
+ if (!mounted) {
113
+ initializeOptions()
114
+ }
115
+ }, [mounted, fetcher, value])
116
+
117
+ useEffect(() => {
118
+ const fetchOptions = async () => {
119
+ try {
120
+ setLoading(true)
121
+ setError(null)
122
+ const data = await fetcher(debouncedSearchTerm)
123
+ setOriginalOptions(data)
124
+ setOptions(data)
125
+ } catch (err) {
126
+ setError(err instanceof Error ? err.message : 'Failed to fetch options')
127
+ } finally {
128
+ setLoading(false)
129
+ }
130
+ }
131
+
132
+ if (!mounted) {
133
+ fetchOptions()
134
+ } else if (!preload) {
135
+ fetchOptions()
136
+ } else if (preload) {
137
+ if (debouncedSearchTerm) {
138
+ setOptions(
139
+ originalOptions.filter((option) =>
140
+ filterFn ? filterFn(option, debouncedSearchTerm) : true
141
+ )
142
+ )
143
+ } else {
144
+ setOptions(originalOptions)
145
+ }
146
+ }
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ }, [fetcher, debouncedSearchTerm, mounted, preload, filterFn])
149
+
150
+ const handleSelect = useCallback(
151
+ (currentValue: string) => {
152
+ if (currentValue !== selectedValue) {
153
+ setSelectedValue(currentValue)
154
+ onChange(currentValue)
155
+ }
156
+ setOpen(false)
157
+ },
158
+ [selectedValue, setSelectedValue, setOpen, onChange]
159
+ )
160
+
161
+ const handleFocus = useCallback(
162
+ (currentValue: string) => {
163
+ if (currentValue !== focusedValue) {
164
+ setFocusedValue(currentValue)
165
+ onFocus(currentValue)
166
+ }
167
+ },
168
+ [focusedValue, setFocusedValue, onFocus]
169
+ )
170
+
171
+ return (
172
+ <div
173
+ className={cn(disabled && 'cursor-not-allowed opacity-50', className)}
174
+ onFocus={() => {
175
+ setOpen(true)
176
+ }}
177
+ onBlur={() => setOpen(false)}
178
+ >
179
+ <Command shouldFilter={false} className="bg-transparent">
180
+ <div>
181
+ <CommandInput
182
+ placeholder={placeholder}
183
+ value={searchTerm}
184
+ className="max-h-8"
185
+ onValueChange={(value) => {
186
+ setSearchTerm(value)
187
+ if (value && !open) setOpen(true)
188
+ }}
189
+ />
190
+ {loading && options.length > 0 && (
191
+ <div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
192
+ <Loader2 className="h-4 w-4 animate-spin" />
193
+ </div>
194
+ )}
195
+ </div>
196
+ <CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
197
+ {error && <div className="text-destructive p-4 text-center">{error}</div>}
198
+ {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
199
+ {!loading &&
200
+ !error &&
201
+ options.length === 0 &&
202
+ (notFound || (
203
+ <CommandEmpty>{noResultsMessage ?? `No ${label.toLowerCase()} found.`}</CommandEmpty>
204
+ ))}
205
+ <CommandGroup>
206
+ {options.map((option, idx) => (
207
+ <>
208
+ <CommandItem
209
+ key={getOptionValue(option) + `${idx}`}
210
+ value={getOptionValue(option)}
211
+ onSelect={handleSelect}
212
+ onMouseEnter={() => handleFocus(getOptionValue(option))}
213
+ className="truncate"
214
+ >
215
+ {renderOption(option)}
216
+ </CommandItem>
217
+ {idx !== options.length - 1 && (
218
+ <div key={idx} className="bg-foreground/10 h-[1px]" />
219
+ )}
220
+ </>
221
+ ))}
222
+ </CommandGroup>
223
+ </CommandList>
224
+ </Command>
225
+ </div>
226
+ )
227
+ }
228
+
229
+ function DefaultLoadingSkeleton() {
230
+ return (
231
+ <CommandGroup>
232
+ <CommandItem disabled>
233
+ <div className="flex w-full items-center gap-2">
234
+ <div className="bg-muted h-6 w-6 animate-pulse rounded-full" />
235
+ <div className="flex flex-1 flex-col gap-1">
236
+ <div className="bg-muted h-4 w-24 animate-pulse rounded" />
237
+ <div className="bg-muted h-3 w-16 animate-pulse rounded" />
238
+ </div>
239
+ </div>
240
+ </CommandItem>
241
+ </CommandGroup>
242
+ )
243
+ }
lightrag/api/graph_viewer_webui/src/components/ui/AsyncSelect.tsx CHANGED
@@ -1,8 +1,9 @@
1
  import { useState, useEffect, useCallback } from 'react'
2
- import { Loader2 } from 'lucide-react'
3
  import { useDebounce } from '@/hooks/useDebounce'
4
 
5
  import { cn } from '@/lib/utils'
 
6
  import {
7
  Command,
8
  CommandEmpty,
@@ -11,6 +12,7 @@ import {
11
  CommandItem,
12
  CommandList
13
  } from '@/components/ui/Command'
 
14
 
15
  export interface Option {
16
  value: string
@@ -31,30 +33,34 @@ export interface AsyncSelectProps<T> {
31
  renderOption: (option: T) => React.ReactNode
32
  /** Function to get the value from an option */
33
  getOptionValue: (option: T) => string
 
 
34
  /** Custom not found message */
35
  notFound?: React.ReactNode
36
  /** Custom loading skeleton */
37
  loadingSkeleton?: React.ReactNode
38
  /** Currently selected value */
39
- value: string | null
40
  /** Callback when selection changes */
41
  onChange: (value: string) => void
42
- /** Callback when focus changes */
43
- onFocus: (value: string) => void
44
  /** Label for the select field */
45
  label: string
46
  /** Placeholder text when no selection */
47
  placeholder?: string
48
  /** Disable the entire select */
49
  disabled?: boolean
50
- /** Custom width for the popover */
51
  width?: string | number
52
  /** Custom class names */
53
  className?: string
54
  /** Custom trigger button class names */
55
  triggerClassName?: string
 
 
56
  /** Custom no results message */
57
  noResultsMessage?: string
 
 
58
  /** Allow clearing the selection */
59
  clearable?: boolean
60
  }
@@ -65,16 +71,20 @@ export function AsyncSelect<T>({
65
  filterFn,
66
  renderOption,
67
  getOptionValue,
 
68
  notFound,
69
  loadingSkeleton,
70
  label,
71
  placeholder = 'Select...',
72
  value,
73
  onChange,
74
- onFocus,
75
  disabled = false,
76
  className,
77
- noResultsMessage
 
 
 
 
78
  }: AsyncSelectProps<T>) {
79
  const [mounted, setMounted] = useState(false)
80
  const [open, setOpen] = useState(false)
@@ -82,7 +92,7 @@ export function AsyncSelect<T>({
82
  const [loading, setLoading] = useState(false)
83
  const [error, setError] = useState<string | null>(null)
84
  const [selectedValue, setSelectedValue] = useState(value)
85
- const [focusedValue, setFocusedValue] = useState<string | null>(null)
86
  const [searchTerm, setSearchTerm] = useState('')
87
  const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
88
  const [originalOptions, setOriginalOptions] = useState<T[]>([])
@@ -92,6 +102,16 @@ export function AsyncSelect<T>({
92
  setSelectedValue(value)
93
  }, [value])
94
 
 
 
 
 
 
 
 
 
 
 
95
  // Effect for initial fetch
96
  useEffect(() => {
97
  const initializeOptions = async () => {
@@ -99,7 +119,7 @@ export function AsyncSelect<T>({
99
  setLoading(true)
100
  setError(null)
101
  // If we have a value, use it for the initial search
102
- const data = value !== null ? await fetcher(value) : []
103
  setOriginalOptions(data)
104
  setOptions(data)
105
  } catch (err) {
@@ -149,75 +169,85 @@ export function AsyncSelect<T>({
149
 
150
  const handleSelect = useCallback(
151
  (currentValue: string) => {
152
- console.log('handleSelect')
153
- if (currentValue !== selectedValue) {
154
- setSelectedValue(currentValue)
155
- onChange(currentValue)
156
- }
157
  setOpen(false)
158
  },
159
- [selectedValue, onChange]
160
- )
161
-
162
- const handleFocus = useCallback(
163
- (currentValue: string) => {
164
- if (currentValue !== focusedValue) {
165
- setFocusedValue(currentValue)
166
- onFocus(currentValue)
167
- }
168
- },
169
- [focusedValue, onFocus]
170
  )
171
 
172
  return (
173
- <div
174
- className={cn(disabled && 'cursor-not-allowed opacity-50', className)}
175
- onFocus={() => setOpen(true)}
176
- onBlur={() => setOpen(false)}
177
- >
178
- <Command shouldFilter={false} className="bg-transparent">
179
- <div className="relative w-full">
180
- <CommandInput
181
- placeholder={placeholder}
182
- value={searchTerm}
183
- onValueChange={(value) => {
184
- setSearchTerm(value)
185
- if (value && !open) setOpen(true)
186
- }}
187
- />
188
- {loading && options.length > 0 && (
189
- <div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
190
- <Loader2 className="h-4 w-4 animate-spin" />
191
- </div>
192
  )}
193
- </div>
194
- <CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
195
- {error && <div className="text-destructive p-4 text-center">{error}</div>}
196
- {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
197
- {!loading &&
198
- !error &&
199
- options.length === 0 &&
200
- (notFound || (
201
- <CommandEmpty>{noResultsMessage ?? `No ${label.toLowerCase()} found.`}</CommandEmpty>
202
- ))}
203
- <CommandGroup>
204
- {options.map((option, idx) => (
205
- <>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  <CommandItem
207
  key={getOptionValue(option)}
208
  value={getOptionValue(option)}
209
  onSelect={handleSelect}
210
- onMouseEnter={() => handleFocus(getOptionValue(option))}
211
  >
212
  {renderOption(option)}
 
 
 
 
 
 
213
  </CommandItem>
214
- {idx !== options.length - 1 && <div className="bg-foreground/10 h-[1px]" />}
215
- </>
216
- ))}
217
- </CommandGroup>
218
- </CommandList>
219
- </Command>
220
- </div>
221
  )
222
  }
223
 
 
1
  import { useState, useEffect, useCallback } from 'react'
2
+ import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
3
  import { useDebounce } from '@/hooks/useDebounce'
4
 
5
  import { cn } from '@/lib/utils'
6
+ import Button from '@/components/ui/Button'
7
  import {
8
  Command,
9
  CommandEmpty,
 
12
  CommandItem,
13
  CommandList
14
  } from '@/components/ui/Command'
15
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
16
 
17
  export interface Option {
18
  value: string
 
33
  renderOption: (option: T) => React.ReactNode
34
  /** Function to get the value from an option */
35
  getOptionValue: (option: T) => string
36
+ /** Function to get the display value for the selected option */
37
+ getDisplayValue: (option: T) => React.ReactNode
38
  /** Custom not found message */
39
  notFound?: React.ReactNode
40
  /** Custom loading skeleton */
41
  loadingSkeleton?: React.ReactNode
42
  /** Currently selected value */
43
+ value: string
44
  /** Callback when selection changes */
45
  onChange: (value: string) => void
 
 
46
  /** Label for the select field */
47
  label: string
48
  /** Placeholder text when no selection */
49
  placeholder?: string
50
  /** Disable the entire select */
51
  disabled?: boolean
52
+ /** Custom width for the popover *
53
  width?: string | number
54
  /** Custom class names */
55
  className?: string
56
  /** Custom trigger button class names */
57
  triggerClassName?: string
58
+ /** Custom search input class names */
59
+ searchInputClassName?: string
60
  /** Custom no results message */
61
  noResultsMessage?: string
62
+ /** Custom trigger tooltip */
63
+ triggerTooltip?: string
64
  /** Allow clearing the selection */
65
  clearable?: boolean
66
  }
 
71
  filterFn,
72
  renderOption,
73
  getOptionValue,
74
+ getDisplayValue,
75
  notFound,
76
  loadingSkeleton,
77
  label,
78
  placeholder = 'Select...',
79
  value,
80
  onChange,
 
81
  disabled = false,
82
  className,
83
+ triggerClassName,
84
+ searchInputClassName,
85
+ noResultsMessage,
86
+ triggerTooltip,
87
+ clearable = true
88
  }: AsyncSelectProps<T>) {
89
  const [mounted, setMounted] = useState(false)
90
  const [open, setOpen] = useState(false)
 
92
  const [loading, setLoading] = useState(false)
93
  const [error, setError] = useState<string | null>(null)
94
  const [selectedValue, setSelectedValue] = useState(value)
95
+ const [selectedOption, setSelectedOption] = useState<T | null>(null)
96
  const [searchTerm, setSearchTerm] = useState('')
97
  const debouncedSearchTerm = useDebounce(searchTerm, preload ? 0 : 150)
98
  const [originalOptions, setOriginalOptions] = useState<T[]>([])
 
102
  setSelectedValue(value)
103
  }, [value])
104
 
105
+ // Initialize selectedOption when options are loaded and value exists
106
+ useEffect(() => {
107
+ if (value && options.length > 0) {
108
+ const option = options.find((opt) => getOptionValue(opt) === value)
109
+ if (option) {
110
+ setSelectedOption(option)
111
+ }
112
+ }
113
+ }, [value, options, getOptionValue])
114
+
115
  // Effect for initial fetch
116
  useEffect(() => {
117
  const initializeOptions = async () => {
 
119
  setLoading(true)
120
  setError(null)
121
  // If we have a value, use it for the initial search
122
+ const data = await fetcher(value)
123
  setOriginalOptions(data)
124
  setOptions(data)
125
  } catch (err) {
 
169
 
170
  const handleSelect = useCallback(
171
  (currentValue: string) => {
172
+ const newValue = clearable && currentValue === selectedValue ? '' : currentValue
173
+ setSelectedValue(newValue)
174
+ setSelectedOption(options.find((option) => getOptionValue(option) === newValue) || null)
175
+ onChange(newValue)
 
176
  setOpen(false)
177
  },
178
+ [selectedValue, onChange, clearable, options, getOptionValue]
 
 
 
 
 
 
 
 
 
 
179
  )
180
 
181
  return (
182
+ <Popover open={open} onOpenChange={setOpen}>
183
+ <PopoverTrigger asChild>
184
+ <Button
185
+ variant="outline"
186
+ role="combobox"
187
+ aria-expanded={open}
188
+ className={cn(
189
+ 'justify-between',
190
+ disabled && 'cursor-not-allowed opacity-50',
191
+ triggerClassName
 
 
 
 
 
 
 
 
 
192
  )}
193
+ disabled={disabled}
194
+ tooltip={triggerTooltip}
195
+ side="bottom"
196
+ >
197
+ {selectedOption ? getDisplayValue(selectedOption) : placeholder}
198
+ <ChevronsUpDown className="opacity-50" size={10} />
199
+ </Button>
200
+ </PopoverTrigger>
201
+ <PopoverContent className={cn('p-0', className)} onCloseAutoFocus={(e) => e.preventDefault()}>
202
+ <Command shouldFilter={false}>
203
+ <div className="relative w-full border-b">
204
+ <CommandInput
205
+ placeholder={`Search ${label.toLowerCase()}...`}
206
+ value={searchTerm}
207
+ onValueChange={(value) => {
208
+ setSearchTerm(value)
209
+ }}
210
+ className={searchInputClassName}
211
+ />
212
+ {loading && options.length > 0 && (
213
+ <div className="absolute top-1/2 right-2 flex -translate-y-1/2 transform items-center">
214
+ <Loader2 className="h-4 w-4 animate-spin" />
215
+ </div>
216
+ )}
217
+ </div>
218
+ <CommandList>
219
+ {error && <div className="text-destructive p-4 text-center">{error}</div>}
220
+ {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
221
+ {!loading &&
222
+ !error &&
223
+ options.length === 0 &&
224
+ (notFound || (
225
+ <CommandEmpty>
226
+ {noResultsMessage ?? `No ${label.toLowerCase()} found.`}
227
+ </CommandEmpty>
228
+ ))}
229
+ <CommandGroup>
230
+ {options.map((option) => (
231
  <CommandItem
232
  key={getOptionValue(option)}
233
  value={getOptionValue(option)}
234
  onSelect={handleSelect}
235
+ className="truncate"
236
  >
237
  {renderOption(option)}
238
+ <Check
239
+ className={cn(
240
+ 'ml-auto h-3 w-3',
241
+ selectedValue === getOptionValue(option) ? 'opacity-100' : 'opacity-0'
242
+ )}
243
+ />
244
  </CommandItem>
245
+ ))}
246
+ </CommandGroup>
247
+ </CommandList>
248
+ </Command>
249
+ </PopoverContent>
250
+ </Popover>
 
251
  )
252
  }
253
 
lightrag/api/graph_viewer_webui/src/hooks/useLightragGraph.tsx CHANGED
@@ -1,10 +1,13 @@
1
  import Graph, { DirectedGraph } from 'graphology'
2
- import { useCallback, useEffect, useState } from 'react'
3
- import { randomColor } from '@/lib/utils'
4
  import * as Constants from '@/lib/constants'
5
  import { useGraphStore, RawGraph } from '@/stores/graph'
6
  import { queryGraphs } from '@/api/lightrag'
7
  import { useBackendState } from '@/stores/state'
 
 
 
8
 
9
  const validateGraph = (graph: RawGraph) => {
10
  if (!graph) {
@@ -53,9 +56,7 @@ const fetchGraph = async (label: string) => {
53
  try {
54
  rawData = await queryGraphs(label)
55
  } catch (e) {
56
- useBackendState
57
- .getState()
58
- .setErrorMessage(e instanceof Error ? e.message : `${e}`, 'Query Graphs Error!')
59
  return null
60
  }
61
 
@@ -69,9 +70,11 @@ const fetchGraph = async (label: string) => {
69
  const node = rawData.nodes[i]
70
  nodeIdMap[node.id] = i
71
 
 
 
 
72
  node.x = Math.random()
73
  node.y = Math.random()
74
- node.color = randomColor()
75
  node.degree = 0
76
  node.size = 10
77
  }
@@ -150,8 +153,10 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
150
  return graph
151
  }
152
 
 
 
153
  const useLightrangeGraph = () => {
154
- const [fetchLabel, setFetchLabel] = useState<string>('*')
155
  const rawGraph = useGraphStore.use.rawGraph()
156
  const sigmaGraph = useGraphStore.use.sigmaGraph()
157
 
@@ -170,12 +175,13 @@ const useLightrangeGraph = () => {
170
  )
171
 
172
  useEffect(() => {
173
- if (fetchLabel) {
174
- const state = useGraphStore.getState()
175
- if (state.queryLabel !== fetchLabel) {
 
176
  state.reset()
177
- fetchGraph(fetchLabel).then((data) => {
178
- state.setQueryLabel(fetchLabel)
179
  state.setSigmaGraph(createSigmaGraph(data))
180
  data?.buildDynamicMap()
181
  state.setRawGraph(data)
@@ -186,7 +192,7 @@ const useLightrangeGraph = () => {
186
  state.reset()
187
  state.setSigmaGraph(new DirectedGraph())
188
  }
189
- }, [fetchLabel])
190
 
191
  const lightrageGraph = useCallback(() => {
192
  if (sigmaGraph) {
@@ -197,7 +203,7 @@ const useLightrangeGraph = () => {
197
  return graph as Graph<NodeType, EdgeType>
198
  }, [sigmaGraph])
199
 
200
- return { lightrageGraph, fetchLabel, setFetchLabel, getNode, getEdge }
201
  }
202
 
203
  export default useLightrangeGraph
 
1
  import Graph, { DirectedGraph } from 'graphology'
2
+ import { useCallback, useEffect } from 'react'
3
+ import { randomColor, errorMessage } from '@/lib/utils'
4
  import * as Constants from '@/lib/constants'
5
  import { useGraphStore, RawGraph } from '@/stores/graph'
6
  import { queryGraphs } from '@/api/lightrag'
7
  import { useBackendState } from '@/stores/state'
8
+ import { useSettingsStore } from '@/stores/settings'
9
+
10
+ import seedrandom from 'seedrandom'
11
 
12
  const validateGraph = (graph: RawGraph) => {
13
  if (!graph) {
 
56
  try {
57
  rawData = await queryGraphs(label)
58
  } catch (e) {
59
+ useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
 
 
60
  return null
61
  }
62
 
 
70
  const node = rawData.nodes[i]
71
  nodeIdMap[node.id] = i
72
 
73
+ // const seed = node.labels.length > 0 ? node.labels[0] : node.id
74
+ seedrandom(node.id, { global: true })
75
+ node.color = randomColor()
76
  node.x = Math.random()
77
  node.y = Math.random()
 
78
  node.degree = 0
79
  node.size = 10
80
  }
 
153
  return graph
154
  }
155
 
156
+ const lastQueryLabel = { label: '' }
157
+
158
  const useLightrangeGraph = () => {
159
+ const queryLabel = useSettingsStore.use.queryLabel()
160
  const rawGraph = useGraphStore.use.rawGraph()
161
  const sigmaGraph = useGraphStore.use.sigmaGraph()
162
 
 
175
  )
176
 
177
  useEffect(() => {
178
+ if (queryLabel) {
179
+ if (lastQueryLabel.label !== queryLabel) {
180
+ lastQueryLabel.label = queryLabel
181
+ const state = useGraphStore.getState()
182
  state.reset()
183
+ fetchGraph(queryLabel).then((data) => {
184
+ // console.debug('Query label: ' + queryLabel)
185
  state.setSigmaGraph(createSigmaGraph(data))
186
  data?.buildDynamicMap()
187
  state.setRawGraph(data)
 
192
  state.reset()
193
  state.setSigmaGraph(new DirectedGraph())
194
  }
195
+ }, [queryLabel])
196
 
197
  const lightrageGraph = useCallback(() => {
198
  if (sigmaGraph) {
 
203
  return graph as Graph<NodeType, EdgeType>
204
  }, [sigmaGraph])
205
 
206
+ return { lightrageGraph, getNode, getEdge }
207
  }
208
 
209
  export default useLightrangeGraph
lightrag/api/graph_viewer_webui/src/lib/constants.ts CHANGED
@@ -19,3 +19,7 @@ export const searchResultLimit = 20
19
 
20
  export const minNodeSize = 4
21
  export const maxNodeSize = 20
 
 
 
 
 
19
 
20
  export const minNodeSize = 4
21
  export const maxNodeSize = 20
22
+
23
+ export const healthCheckInterval = 15 // seconds
24
+
25
+ export const defaultQueryLabel = '*'
lightrag/api/graph_viewer_webui/src/lib/utils.ts CHANGED
@@ -15,6 +15,10 @@ export function randomColor() {
15
  return code
16
  }
17
 
 
 
 
 
18
  type WithSelectors<S> = S extends { getState: () => infer T }
19
  ? S & { use: { [K in keyof T]: () => T[K] } }
20
  : never
 
15
  return code
16
  }
17
 
18
+ export function errorMessage(error: any) {
19
+ return error instanceof Error ? error.message : `${error}`
20
+ }
21
+
22
  type WithSelectors<S> = S extends { getState: () => infer T }
23
  ? S & { use: { [K in keyof T]: () => T[K] } }
24
  : never
lightrag/api/graph_viewer_webui/src/stores/graph.ts CHANGED
@@ -63,7 +63,6 @@ interface GraphState {
63
  selectedEdge: string | null
64
  focusedEdge: string | null
65
 
66
- queryLabel: string | null
67
  rawGraph: RawGraph | null
68
  sigmaGraph: DirectedGraph | null
69
 
@@ -78,7 +77,6 @@ interface GraphState {
78
 
79
  setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
80
 
81
- setQueryLabel: (queryLabel: string | null) => void
82
  setRawGraph: (rawGraph: RawGraph | null) => void
83
  setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
84
  }
@@ -91,7 +89,6 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
91
 
92
  moveToSelectedNode: false,
93
 
94
- queryLabel: null,
95
  rawGraph: null,
96
  sigmaGraph: null,
97
 
@@ -113,17 +110,11 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
113
  focusedNode: null,
114
  selectedEdge: null,
115
  focusedEdge: null,
116
- queryLabel: null,
117
  rawGraph: null,
118
  sigmaGraph: null,
119
  moveToSelectedNode: false
120
  }),
121
 
122
- setQueryLabel: (queryLabel: string | null) =>
123
- set({
124
- queryLabel
125
- }),
126
-
127
  setRawGraph: (rawGraph: RawGraph | null) =>
128
  set({
129
  rawGraph
 
63
  selectedEdge: string | null
64
  focusedEdge: string | null
65
 
 
66
  rawGraph: RawGraph | null
67
  sigmaGraph: DirectedGraph | null
68
 
 
77
 
78
  setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
79
 
 
80
  setRawGraph: (rawGraph: RawGraph | null) => void
81
  setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
82
  }
 
89
 
90
  moveToSelectedNode: false,
91
 
 
92
  rawGraph: null,
93
  sigmaGraph: null,
94
 
 
110
  focusedNode: null,
111
  selectedEdge: null,
112
  focusedEdge: null,
 
113
  rawGraph: null,
114
  sigmaGraph: null,
115
  moveToSelectedNode: false
116
  }),
117
 
 
 
 
 
 
118
  setRawGraph: (rawGraph: RawGraph | null) =>
119
  set({
120
  rawGraph
lightrag/api/graph_viewer_webui/src/stores/settings.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { create } from 'zustand'
2
  import { persist, createJSONStorage } from 'zustand/middleware'
3
  import { createSelectors } from '@/lib/utils'
 
4
 
5
  type Theme = 'dark' | 'light' | 'system'
6
 
@@ -10,7 +11,11 @@ interface SettingsState {
10
  enableEdgeEvents: boolean
11
  enableHideUnselectedEdges: boolean
12
  showEdgeLabel: boolean
 
13
  setTheme: (theme: Theme) => void
 
 
 
14
  }
15
 
16
  const useSettingsStoreBase = create<SettingsState>()(
@@ -22,16 +27,26 @@ const useSettingsStoreBase = create<SettingsState>()(
22
  enableHideUnselectedEdges: true,
23
  showEdgeLabel: false,
24
 
25
- setTheme: (theme: Theme) => set({ theme })
 
 
 
 
 
 
 
26
  }),
27
  {
28
  name: 'settings-storage',
29
  storage: createJSONStorage(() => localStorage),
30
- version: 2,
31
  migrate: (state: any, version: number) => {
32
  if (version < 2) {
33
  state.showEdgeLabel = false
34
  }
 
 
 
35
  }
36
  }
37
  )
 
1
  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
 
 
11
  enableEdgeEvents: boolean
12
  enableHideUnselectedEdges: boolean
13
  showEdgeLabel: boolean
14
+
15
  setTheme: (theme: Theme) => void
16
+
17
+ queryLabel: string
18
+ setQueryLabel: (queryLabel: string) => void
19
  }
20
 
21
  const useSettingsStoreBase = create<SettingsState>()(
 
27
  enableHideUnselectedEdges: true,
28
  showEdgeLabel: false,
29
 
30
+ queryLabel: defaultQueryLabel,
31
+
32
+ setTheme: (theme: Theme) => set({ theme }),
33
+
34
+ setQueryLabel: (queryLabel: string) =>
35
+ set({
36
+ queryLabel
37
+ })
38
  }),
39
  {
40
  name: 'settings-storage',
41
  storage: createJSONStorage(() => localStorage),
42
+ version: 3,
43
  migrate: (state: any, version: number) => {
44
  if (version < 2) {
45
  state.showEdgeLabel = false
46
  }
47
+ if (version < 3) {
48
+ state.queryLabel = defaultQueryLabel
49
+ }
50
  }
51
  }
52
  )