File size: 6,016 Bytes
5cfaefc
3578090
 
07eaad0
6043dbd
3578090
77ca676
6043dbd
 
3578090
 
77ca676
3578090
dcae38a
75d0b6d
b79b057
f57fcd2
bd39ed2
5edb4ed
07eaad0
 
 
 
 
 
 
 
 
3578090
07eaad0
 
3578090
07eaad0
dcae38a
07eaad0
3578090
07eaad0
dcae38a
07eaad0
 
dcae38a
07eaad0
 
 
5edb4ed
07eaad0
 
 
9273670
85b7cce
9273670
 
 
311e37f
9273670
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3578090
 
07eaad0
 
dcae38a
3578090
3be437a
3578090
 
1600ec8
5cfaefc
b79b057
 
75d0b6d
 
1600ec8
 
75d0b6d
 
 
1600ec8
 
 
75d0b6d
 
5cfaefc
75d0b6d
 
5cfaefc
6043dbd
f57fcd2
abf0e97
 
e70144e
f57fcd2
 
e70144e
f57fcd2
 
e70144e
f57fcd2
 
 
 
 
 
 
 
 
 
 
6043dbd
 
 
f57fcd2
 
 
 
 
 
6860011
f57fcd2
 
 
6043dbd
6860011
6043dbd
 
 
 
 
 
 
 
 
 
 
 
f57fcd2
6043dbd
 
 
f57fcd2
6043dbd
 
bd39ed2
6043dbd
f57fcd2
6043dbd
bd39ed2
f57fcd2
 
e70144e
f57fcd2
 
6043dbd
 
 
 
3578090
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import { useCallback, useEffect } from 'react'
import { AsyncSelect } from '@/components/ui/AsyncSelect'
import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph'
import { labelListLimit, controlButtonVariant } from '@/lib/constants'
import MiniSearch from 'minisearch'
import { useTranslation } from 'react-i18next'
import { RefreshCw } from 'lucide-react'
import Button from '@/components/ui/Button'

const GraphLabels = () => {
  const { t } = useTranslation()
  const label = useSettingsStore.use.queryLabel()
  const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
  const labelsFetchAttempted = useGraphStore.use.labelsFetchAttempted()

  // Remove initial label fetch effect as it's now handled by fetchGraph based on lastSuccessfulQueryLabel

  const getSearchEngine = useCallback(() => {
    // Create search engine
    const searchEngine = new MiniSearch({
      idField: 'id',
      fields: ['value'],
      searchOptions: {
        prefix: true,
        fuzzy: 0.2,
        boost: {
          label: 2
        }
      }
    })

    // Add documents
    const documents = allDatabaseLabels.map((str, index) => ({ id: index, value: str }))
    searchEngine.addAll(documents)

    return {
      labels: allDatabaseLabels,
      searchEngine
    }
  }, [allDatabaseLabels])

  const fetchData = useCallback(
    async (query?: string): Promise<string[]> => {
      const { labels, searchEngine } = getSearchEngine()

      let result: string[] = labels
      if (query) {
        // Search labels using MiniSearch
        result = searchEngine.search(query).map((r: { id: number }) => labels[r.id])

        // Add middle-content matching if results are few
        // This enables matching content in the middle of text, not just from the beginning
        if (result.length < 15) {
          // Get already matched labels to avoid duplicates
          const matchedLabels = new Set(result)

          // Perform middle-content matching on all labels
          const middleMatchResults = labels.filter(label => {
            // Skip already matched labels
            if (matchedLabels.has(label)) return false

            // Match if label contains query string but doesn't start with it
            return label &&
                   typeof label === 'string' &&
                   !label.toLowerCase().startsWith(query.toLowerCase()) &&
                   label.toLowerCase().includes(query.toLowerCase())
          })

          // Merge results
          result = [...result, ...middleMatchResults]
        }
      }

      return result.length <= labelListLimit
        ? result
        : [...result.slice(0, labelListLimit), '...']
    },
    [getSearchEngine]
  )

  // Validate label
  useEffect(() => {

    if (labelsFetchAttempted) {
      if (allDatabaseLabels.length > 1) {
        if (label && label !== '*' && !allDatabaseLabels.includes(label)) {
          console.log(`Label "${label}" not in available labels, setting to "*"`);
          useSettingsStore.getState().setQueryLabel('*');
        } else {
          console.log(`Label "${label}" is valid`);
        }
      } else if (label && allDatabaseLabels.length <= 1 && label && label !== '*') {
        console.log('Available labels list is empty, setting label to empty');
        useSettingsStore.getState().setQueryLabel('');
      }
      useGraphStore.getState().setLabelsFetchAttempted(false)
    }

  }, [allDatabaseLabels, label, labelsFetchAttempted]);

  const handleRefresh = useCallback(() => {
    // Reset fetch status flags
    useGraphStore.getState().setLabelsFetchAttempted(false)
    useGraphStore.getState().setGraphDataFetchAttempted(false)

    // Clear last successful query label to ensure labels are fetched
    useGraphStore.getState().setLastSuccessfulQueryLabel('')

    // Get current label
    const currentLabel = useSettingsStore.getState().queryLabel

    // If current label is empty, use default label '*'
    if (!currentLabel) {
      useSettingsStore.getState().setQueryLabel('*')
    } else {
      // Trigger data reload
      useSettingsStore.getState().setQueryLabel('')
      setTimeout(() => {
        useSettingsStore.getState().setQueryLabel(currentLabel)
      }, 0)
    }
  }, []);

  return (
    <div className="flex items-center">
      {/* Always show refresh button */}
      <Button
        size="icon"
        variant={controlButtonVariant}
        onClick={handleRefresh}
        tooltip={t('graphPanel.graphLabels.refreshTooltip')}
        className="mr-2"
      >
        <RefreshCw className="h-4 w-4" />
      </Button>
      <AsyncSelect<string>
        className="min-w-[300px]"
        triggerClassName="max-h-8"
        searchInputClassName="max-h-8"
        triggerTooltip={t('graphPanel.graphLabels.selectTooltip')}
        fetcher={fetchData}
        renderOption={(item) => <div>{item}</div>}
        getOptionValue={(item) => item}
        getDisplayValue={(item) => <div>{item}</div>}
        notFound={<div className="py-6 text-center text-sm">No labels found</div>}
        label={t('graphPanel.graphLabels.label')}
        placeholder={t('graphPanel.graphLabels.placeholder')}
        value={label !== null ? label : '*'}
        onChange={(newLabel) => {
          const currentLabel = useSettingsStore.getState().queryLabel;

          // select the last item means query all
          if (newLabel === '...') {
            newLabel = '*';
          }

          // Handle reselecting the same label
          if (newLabel === currentLabel && newLabel !== '*') {
            newLabel = '*';
          }

          // Reset graphDataFetchAttempted flag to ensure data fetch is triggered
          useGraphStore.getState().setGraphDataFetchAttempted(false);

          // Update the label to trigger data loading
          useSettingsStore.getState().setQueryLabel(newLabel);
        }}
        clearable={false}  // Prevent clearing value on reselect
      />
    </div>
  )
}

export default GraphLabels