@nextclaw/ui 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>nextclaw - 系统配置</title>
8
- <script type="module" crossorigin src="/assets/index-BivRvIey.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-JpepB1WI.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-iSLahgqA.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api/config.ts CHANGED
@@ -2,6 +2,7 @@ import { api } from './client';
2
2
  import type {
3
3
  ConfigView,
4
4
  ConfigMetaView,
5
+ ConfigSchemaResponse,
5
6
  ProviderConfigView,
6
7
  ChannelConfigUpdate,
7
8
  ProviderConfigUpdate,
@@ -26,6 +27,15 @@ export async function fetchConfigMeta(): Promise<ConfigMetaView> {
26
27
  return response.data;
27
28
  }
28
29
 
30
+ // GET /api/config/schema
31
+ export async function fetchConfigSchema(): Promise<ConfigSchemaResponse> {
32
+ const response = await api.get<ConfigSchemaResponse>('/api/config/schema');
33
+ if (!response.ok) {
34
+ throw new Error(response.error.message);
35
+ }
36
+ return response.data;
37
+ }
38
+
29
39
  // PUT /api/config/model
30
40
  export async function updateModel(data: {
31
41
  model: string;
package/src/api/types.ts CHANGED
@@ -80,6 +80,25 @@ export type ConfigMetaView = {
80
80
  channels: ChannelSpecView[];
81
81
  };
82
82
 
83
+ export type ConfigUiHint = {
84
+ label?: string;
85
+ help?: string;
86
+ group?: string;
87
+ order?: number;
88
+ advanced?: boolean;
89
+ sensitive?: boolean;
90
+ placeholder?: string;
91
+ };
92
+
93
+ export type ConfigUiHints = Record<string, ConfigUiHint>;
94
+
95
+ export type ConfigSchemaResponse = {
96
+ schema: Record<string, unknown>;
97
+ uiHints: ConfigUiHints;
98
+ version: string;
99
+ generatedAt: string;
100
+ };
101
+
83
102
  export type FeishuProbeView = {
84
103
  appId: string;
85
104
  botName?: string | null;
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { useConfig, useUpdateChannel } from '@/hooks/useConfig';
2
+ import { useConfig, useConfigSchema, useUpdateChannel } from '@/hooks/useConfig';
3
3
  import { probeFeishu } from '@/api/config';
4
4
  import { useUiStore } from '@/stores/ui.store';
5
5
  import {
@@ -16,6 +16,7 @@ import { Label } from '@/components/ui/label';
16
16
  import { Switch } from '@/components/ui/switch';
17
17
  import { TagInput } from '@/components/common/TagInput';
18
18
  import { t } from '@/lib/i18n';
19
+ import { hintForPath } from '@/lib/config-hints';
19
20
  import { toast } from 'sonner';
20
21
  import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from 'lucide-react';
21
22
 
@@ -122,6 +123,7 @@ const channelColors: Record<string, string> = {
122
123
  export function ChannelForm() {
123
124
  const { channelModal, closeChannelModal } = useUiStore();
124
125
  const { data: config } = useConfig();
126
+ const { data: schema } = useConfigSchema();
125
127
  const updateChannel = useUpdateChannel();
126
128
 
127
129
  const [formData, setFormData] = useState<Record<string, unknown>>({});
@@ -130,6 +132,10 @@ export function ChannelForm() {
130
132
  const channelName = channelModal.channel;
131
133
  const channelConfig = channelName ? config?.channels[channelName] : null;
132
134
  const fields = channelName ? CHANNEL_FIELDS[channelName] : [];
135
+ const uiHints = schema?.uiHints;
136
+ const channelLabel = channelName
137
+ ? hintForPath(`channels.${channelName}`, uiHints)?.label ?? channelName
138
+ : channelName;
133
139
 
134
140
  useEffect(() => {
135
141
  if (channelConfig) {
@@ -186,7 +192,7 @@ export function ChannelForm() {
186
192
  <Icon className="h-5 w-5 text-white" />
187
193
  </div>
188
194
  <div>
189
- <DialogTitle className="capitalize">{channelName}</DialogTitle>
195
+ <DialogTitle className="capitalize">{channelLabel}</DialogTitle>
190
196
  <DialogDescription>Configure message channel parameters</DialogDescription>
191
197
  </div>
192
198
  </div>
@@ -194,14 +200,21 @@ export function ChannelForm() {
194
200
 
195
201
  <div className="flex-1 overflow-y-auto custom-scrollbar py-2">
196
202
  <form onSubmit={handleSubmit} className="space-y-5 pr-2">
197
- {fields.map((field) => (
198
- <div key={field.name} className="space-y-2.5">
203
+ {fields.map((field) => {
204
+ const hint = channelName
205
+ ? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
206
+ : undefined;
207
+ const label = hint?.label ?? field.label;
208
+ const placeholder = hint?.placeholder;
209
+
210
+ return (
211
+ <div key={field.name} className="space-y-2.5">
199
212
  <Label
200
213
  htmlFor={field.name}
201
214
  className="text-sm font-medium text-gray-900 flex items-center gap-2"
202
215
  >
203
216
  {getFieldIcon(field.name)}
204
- {field.label}
217
+ {label}
205
218
  </Label>
206
219
 
207
220
  {field.type === 'boolean' && (
@@ -224,6 +237,7 @@ export function ChannelForm() {
224
237
  type={field.type}
225
238
  value={(formData[field.name] as string) || ''}
226
239
  onChange={(e) => updateField(field.name, e.target.value)}
240
+ placeholder={placeholder}
227
241
  className="rounded-xl"
228
242
  />
229
243
  )}
@@ -234,7 +248,7 @@ export function ChannelForm() {
234
248
  type="password"
235
249
  value={(formData[field.name] as string) || ''}
236
250
  onChange={(e) => updateField(field.name, e.target.value)}
237
- placeholder="Leave blank to keep unchanged"
251
+ placeholder={placeholder ?? 'Leave blank to keep unchanged'}
238
252
  className="rounded-xl"
239
253
  />
240
254
  )}
@@ -245,6 +259,7 @@ export function ChannelForm() {
245
259
  type="number"
246
260
  value={(formData[field.name] as number) || 0}
247
261
  onChange={(e) => updateField(field.name, parseInt(e.target.value) || 0)}
262
+ placeholder={placeholder}
248
263
  className="rounded-xl"
249
264
  />
250
265
  )}
@@ -255,8 +270,9 @@ export function ChannelForm() {
255
270
  onChange={(tags) => updateField(field.name, tags)}
256
271
  />
257
272
  )}
258
- </div>
259
- ))}
273
+ </div>
274
+ );
275
+ })}
260
276
 
261
277
  <DialogFooter className="pt-4">
262
278
  <Button
@@ -1,4 +1,4 @@
1
- import { useConfig, useConfigMeta } from '@/hooks/useConfig';
1
+ import { useConfig, useConfigMeta, useConfigSchema } from '@/hooks/useConfig';
2
2
  import { Button } from '@/components/ui/button';
3
3
  import { MessageCircle, Mail, MessageSquare, Slack, ExternalLink, Bell, Zap, Radio } from 'lucide-react';
4
4
  import { useState } from 'react';
@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils';
8
8
  import { Tabs } from '@/components/ui/tabs-custom';
9
9
  import { LogoBadge } from '@/components/common/LogoBadge';
10
10
  import { getChannelLogo } from '@/lib/logos';
11
+ import { hintForPath } from '@/lib/config-hints';
11
12
 
12
13
  const channelIcons: Record<string, typeof MessageCircle> = {
13
14
  telegram: MessageCircle,
@@ -29,8 +30,10 @@ const channelDescriptions: Record<string, string> = {
29
30
  export function ChannelsList() {
30
31
  const { data: config } = useConfig();
31
32
  const { data: meta } = useConfigMeta();
33
+ const { data: schema } = useConfigSchema();
32
34
  const { openChannelModal } = useUiStore();
33
35
  const [activeTab, setActiveTab] = useState('active');
36
+ const uiHints = schema?.uiHints;
34
37
 
35
38
  if (!config || !meta) {
36
39
  return <div className="p-8 text-gray-400">Loading channels...</div>;
@@ -60,6 +63,11 @@ export function ChannelsList() {
60
63
  const channelConfig = config.channels[channel.name];
61
64
  const enabled = channelConfig?.enabled || false;
62
65
  const Icon = channelIcons[channel.name] || channelIcons.default;
66
+ const channelHint = hintForPath(`channels.${channel.name}`, uiHints);
67
+ const description =
68
+ channelHint?.help ||
69
+ channelDescriptions[channel.name] ||
70
+ 'Configure this communication channel';
63
71
 
64
72
  return (
65
73
  <div
@@ -108,7 +116,7 @@ export function ChannelsList() {
108
116
  {channel.displayName || channel.name}
109
117
  </h3>
110
118
  <p className="text-[12px] text-gray-500 leading-relaxed line-clamp-2">
111
- {channelDescriptions[channel.name] || 'Configure this communication channel'}
119
+ {description}
112
120
  </p>
113
121
  </div>
114
122
 
@@ -3,18 +3,25 @@ import { Card } from '@/components/ui/card';
3
3
  import { Input } from '@/components/ui/input';
4
4
  import { Label } from '@/components/ui/label';
5
5
  import { Skeleton } from '@/components/ui/skeleton';
6
- import { useConfig, useUpdateModel } from '@/hooks/useConfig';
6
+ import { useConfig, useConfigSchema, useUpdateModel } from '@/hooks/useConfig';
7
+ import { hintForPath } from '@/lib/config-hints';
7
8
  import { Folder, Loader2, Sliders, Sparkles } from 'lucide-react';
8
9
  import { useEffect, useState } from 'react';
9
10
 
10
11
  export function ModelConfig() {
11
12
  const { data: config, isLoading } = useConfig();
13
+ const { data: schema } = useConfigSchema();
12
14
  const updateModel = useUpdateModel();
13
15
 
14
16
  const [model, setModel] = useState('');
15
17
  const [workspace, setWorkspace] = useState('');
16
18
  const [maxTokens, setMaxTokens] = useState(8192);
17
19
  const [temperature, setTemperature] = useState(0.7);
20
+ const uiHints = schema?.uiHints;
21
+ const modelHint = hintForPath('agents.defaults.model', uiHints);
22
+ const workspaceHint = hintForPath('agents.defaults.workspace', uiHints);
23
+ const maxTokensHint = hintForPath('agents.defaults.maxTokens', uiHints);
24
+ const temperatureHint = hintForPath('agents.defaults.temperature', uiHints);
18
25
 
19
26
  useEffect(() => {
20
27
  if (config?.agents?.defaults) {
@@ -85,15 +92,19 @@ export function ModelConfig() {
85
92
  </div>
86
93
 
87
94
  <div className="space-y-2">
88
- <Label htmlFor="model" className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Model Name</Label>
95
+ <Label htmlFor="model" className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
96
+ {modelHint?.label ?? 'Model Name'}
97
+ </Label>
89
98
  <Input
90
99
  id="model"
91
100
  value={model}
92
101
  onChange={(e) => setModel(e.target.value)}
93
- placeholder="minimax/MiniMax-M2.1"
102
+ placeholder={modelHint?.placeholder ?? 'minimax/MiniMax-M2.1'}
94
103
  className="h-12 px-4 rounded-xl"
95
104
  />
96
- <p className="text-xs text-gray-400">Examples: minimax/MiniMax-M2.1 · anthropic/claude-opus-4-5</p>
105
+ <p className="text-xs text-gray-400">
106
+ {modelHint?.help ?? 'Examples: minimax/MiniMax-M2.1 · anthropic/claude-opus-4-5'}
107
+ </p>
97
108
  </div>
98
109
  </div>
99
110
 
@@ -107,12 +118,14 @@ export function ModelConfig() {
107
118
  </div>
108
119
 
109
120
  <div className="space-y-2">
110
- <Label htmlFor="workspace" className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Default Path</Label>
121
+ <Label htmlFor="workspace" className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
122
+ {workspaceHint?.label ?? 'Default Path'}
123
+ </Label>
111
124
  <Input
112
125
  id="workspace"
113
126
  value={workspace}
114
127
  onChange={(e) => setWorkspace(e.target.value)}
115
- placeholder="/path/to/workspace"
128
+ placeholder={workspaceHint?.placeholder ?? '/path/to/workspace'}
116
129
  className="h-12 px-4 rounded-xl"
117
130
  />
118
131
  </div>
@@ -131,7 +144,9 @@ export function ModelConfig() {
131
144
  <div className="grid grid-cols-1 md:grid-cols-2 gap-12">
132
145
  <div className="space-y-4">
133
146
  <div className="flex justify-between items-center mb-2">
134
- <Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Max Tokens</Label>
147
+ <Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
148
+ {maxTokensHint?.label ?? 'Max Tokens'}
149
+ </Label>
135
150
  <span className="text-sm font-semibold text-gray-900">{maxTokens.toLocaleString()}</span>
136
151
  </div>
137
152
  <input
@@ -147,7 +162,9 @@ export function ModelConfig() {
147
162
 
148
163
  <div className="space-y-4">
149
164
  <div className="flex justify-between items-center mb-2">
150
- <Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Temperature</Label>
165
+ <Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
166
+ {temperatureHint?.label ?? 'Temperature'}
167
+ </Label>
151
168
  <span className="text-sm font-semibold text-gray-900">{temperature}</span>
152
169
  </div>
153
170
  <input
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { useConfig, useConfigMeta, useUpdateProvider } from '@/hooks/useConfig';
2
+ import { useConfig, useConfigMeta, useConfigSchema, useUpdateProvider } from '@/hooks/useConfig';
3
3
  import { useUiStore } from '@/stores/ui.store';
4
4
  import {
5
5
  Dialog,
@@ -15,6 +15,7 @@ import { Label } from '@/components/ui/label';
15
15
  import { MaskedInput } from '@/components/common/MaskedInput';
16
16
  import { KeyValueEditor } from '@/components/common/KeyValueEditor';
17
17
  import { t } from '@/lib/i18n';
18
+ import { hintForPath } from '@/lib/config-hints';
18
19
  import type { ProviderConfigUpdate } from '@/api/types';
19
20
  import { KeyRound, Globe, Hash } from 'lucide-react';
20
21
 
@@ -22,6 +23,7 @@ export function ProviderForm() {
22
23
  const { providerModal, closeProviderModal } = useUiStore();
23
24
  const { data: config } = useConfig();
24
25
  const { data: meta } = useConfigMeta();
26
+ const { data: schema } = useConfigSchema();
25
27
  const updateProvider = useUpdateProvider();
26
28
 
27
29
  const [apiKey, setApiKey] = useState('');
@@ -32,6 +34,11 @@ export function ProviderForm() {
32
34
  const providerName = providerModal.provider;
33
35
  const providerSpec = meta?.providers.find((p) => p.name === providerName);
34
36
  const providerConfig = providerName ? config?.providers[providerName] : null;
37
+ const uiHints = schema?.uiHints;
38
+ const apiKeyHint = providerName ? hintForPath(`providers.${providerName}.apiKey`, uiHints) : undefined;
39
+ const apiBaseHint = providerName ? hintForPath(`providers.${providerName}.apiBase`, uiHints) : undefined;
40
+ const extraHeadersHint = providerName ? hintForPath(`providers.${providerName}.extraHeaders`, uiHints) : undefined;
41
+ const wireApiHint = providerName ? hintForPath(`providers.${providerName}.wireApi`, uiHints) : undefined;
35
42
 
36
43
  useEffect(() => {
37
44
  if (providerConfig) {
@@ -97,14 +104,18 @@ export function ProviderForm() {
97
104
  <div className="space-y-2.5">
98
105
  <Label htmlFor="apiKey" className="text-sm font-medium text-gray-900 flex items-center gap-2">
99
106
  <KeyRound className="h-3.5 w-3.5 text-gray-500" />
100
- {t('apiKey')}
107
+ {apiKeyHint?.label ?? t('apiKey')}
101
108
  </Label>
102
109
  <MaskedInput
103
110
  id="apiKey"
104
111
  value={apiKey}
105
112
  isSet={providerConfig?.apiKeySet}
106
113
  onChange={(e) => setApiKey(e.target.value)}
107
- placeholder={providerConfig?.apiKeySet ? t('apiKeySet') : 'Enter API Key'}
114
+ placeholder={
115
+ providerConfig?.apiKeySet
116
+ ? t('apiKeySet')
117
+ : apiKeyHint?.placeholder ?? 'Enter API Key'
118
+ }
108
119
  className="rounded-xl"
109
120
  />
110
121
  </div>
@@ -112,24 +123,28 @@ export function ProviderForm() {
112
123
  <div className="space-y-2.5">
113
124
  <Label htmlFor="apiBase" className="text-sm font-medium text-gray-900 flex items-center gap-2">
114
125
  <Globe className="h-3.5 w-3.5 text-gray-500" />
115
- {t('apiBase')}
126
+ {apiBaseHint?.label ?? t('apiBase')}
116
127
  </Label>
117
128
  <Input
118
129
  id="apiBase"
119
130
  type="text"
120
131
  value={apiBase}
121
132
  onChange={(e) => setApiBase(e.target.value)}
122
- placeholder={providerSpec?.defaultApiBase || 'https://api.example.com'}
133
+ placeholder={
134
+ providerSpec?.defaultApiBase ||
135
+ apiBaseHint?.placeholder ||
136
+ 'https://api.example.com'
137
+ }
123
138
  className="rounded-xl"
124
139
  />
125
140
  </div>
126
141
 
127
142
  {providerSpec?.supportsWireApi && (
128
143
  <div className="space-y-2.5">
129
- <Label htmlFor="wireApi" className="text-sm font-medium text-gray-900 flex items-center gap-2">
130
- <Hash className="h-3.5 w-3.5 text-gray-500" />
131
- {t('wireApi')}
132
- </Label>
144
+ <Label htmlFor="wireApi" className="text-sm font-medium text-gray-900 flex items-center gap-2">
145
+ <Hash className="h-3.5 w-3.5 text-gray-500" />
146
+ {wireApiHint?.label ?? t('wireApi')}
147
+ </Label>
133
148
  <select
134
149
  id="wireApi"
135
150
  value={wireApi}
@@ -152,7 +167,7 @@ export function ProviderForm() {
152
167
  <div className="space-y-2.5">
153
168
  <Label className="text-sm font-medium text-gray-900 flex items-center gap-2">
154
169
  <Hash className="h-3.5 w-3.5 text-gray-500" />
155
- {t('extraHeaders')}
170
+ {extraHeadersHint?.label ?? t('extraHeaders')}
156
171
  </Label>
157
172
  <KeyValueEditor
158
173
  value={extraHeaders}
@@ -1,4 +1,4 @@
1
- import { useConfig, useConfigMeta } from '@/hooks/useConfig';
1
+ import { useConfig, useConfigMeta, useConfigSchema } from '@/hooks/useConfig';
2
2
  import { Button } from '@/components/ui/button';
3
3
  import { KeyRound, Check, Settings } from 'lucide-react';
4
4
  import { useState } from 'react';
@@ -8,12 +8,15 @@ import { cn } from '@/lib/utils';
8
8
  import { Tabs } from '@/components/ui/tabs-custom';
9
9
  import { LogoBadge } from '@/components/common/LogoBadge';
10
10
  import { getProviderLogo } from '@/lib/logos';
11
+ import { hintForPath } from '@/lib/config-hints';
11
12
 
12
13
  export function ProvidersList() {
13
14
  const { data: config } = useConfig();
14
15
  const { data: meta } = useConfigMeta();
16
+ const { data: schema } = useConfigSchema();
15
17
  const { openProviderModal } = useUiStore();
16
18
  const [activeTab, setActiveTab] = useState('installed');
19
+ const uiHints = schema?.uiHints;
17
20
 
18
21
  if (!config || !meta) {
19
22
  return <div className="p-8">Loading...</div>;
@@ -41,6 +44,8 @@ export function ProvidersList() {
41
44
  {filteredProviders.map((provider) => {
42
45
  const providerConfig = config.providers[provider.name];
43
46
  const hasConfig = providerConfig?.apiKeySet;
47
+ const providerHint = hintForPath(`providers.${provider.name}`, uiHints);
48
+ const description = providerHint?.help || 'Configure AI services for your agents';
44
49
 
45
50
  return (
46
51
  <div
@@ -96,9 +101,7 @@ export function ProvidersList() {
96
101
  {provider.displayName || provider.name}
97
102
  </h3>
98
103
  <p className="text-[12px] text-gray-500 leading-relaxed line-clamp-2">
99
- {provider.name === 'openai'
100
- ? 'Leading AI models including GPT-4 and GPT-3.5'
101
- : 'Configure AI services for your agents'}
104
+ {description}
102
105
  </p>
103
106
  </div>
104
107
 
@@ -1,5 +1,12 @@
1
1
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2
- import { fetchConfig, fetchConfigMeta, updateModel, updateProvider, updateChannel } from '@/api/config';
2
+ import {
3
+ fetchConfig,
4
+ fetchConfigMeta,
5
+ fetchConfigSchema,
6
+ updateModel,
7
+ updateProvider,
8
+ updateChannel
9
+ } from '@/api/config';
3
10
  import { toast } from 'sonner';
4
11
  import { t } from '@/lib/i18n';
5
12
 
@@ -20,6 +27,14 @@ export function useConfigMeta() {
20
27
  });
21
28
  }
22
29
 
30
+ export function useConfigSchema() {
31
+ return useQuery({
32
+ queryKey: ['config-schema'],
33
+ queryFn: fetchConfigSchema,
34
+ staleTime: Infinity
35
+ });
36
+ }
37
+
23
38
  export function useUpdateModel() {
24
39
  const queryClient = useQueryClient();
25
40
 
@@ -0,0 +1,43 @@
1
+ import type { ConfigUiHints, ConfigUiHint } from '@/api/types';
2
+
3
+ function normalizePath(path: string | Array<string | number>): string {
4
+ if (Array.isArray(path)) {
5
+ return path.filter((segment) => typeof segment === 'string').join('.');
6
+ }
7
+ return path;
8
+ }
9
+
10
+ export function hintForPath(
11
+ path: string | Array<string | number>,
12
+ hints?: ConfigUiHints
13
+ ): ConfigUiHint | undefined {
14
+ if (!hints) {
15
+ return undefined;
16
+ }
17
+ const key = normalizePath(path);
18
+ const direct = hints[key];
19
+ if (direct) {
20
+ return direct;
21
+ }
22
+ const segments = key.split('.');
23
+ for (const [hintKey, hint] of Object.entries(hints)) {
24
+ if (!hintKey.includes('*')) {
25
+ continue;
26
+ }
27
+ const hintSegments = hintKey.split('.');
28
+ if (hintSegments.length !== segments.length) {
29
+ continue;
30
+ }
31
+ let match = true;
32
+ for (let i = 0; i < segments.length; i += 1) {
33
+ if (hintSegments[i] !== '*' && hintSegments[i] !== segments[i]) {
34
+ match = false;
35
+ break;
36
+ }
37
+ }
38
+ if (match) {
39
+ return hint;
40
+ }
41
+ }
42
+ return undefined;
43
+ }