@nextclaw/ui 0.3.1 → 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/.eslintrc.cjs +3 -1
- package/CHANGELOG.md +12 -0
- package/dist/assets/index-JpepB1WI.js +225 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/api/config.ts +10 -0
- package/src/api/types.ts +32 -0
- package/src/components/config/ChannelForm.tsx +24 -8
- package/src/components/config/ChannelsList.tsx +10 -2
- package/src/components/config/ModelConfig.tsx +25 -8
- package/src/components/config/ProviderForm.tsx +25 -10
- package/src/components/config/ProvidersList.tsx +7 -4
- package/src/hooks/useConfig.ts +16 -1
- package/src/lib/config-hints.ts +43 -0
- package/dist/assets/index-BivRvIey.js +0 -225
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-
|
|
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
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
|
@@ -35,6 +35,19 @@ export type ConfigView = {
|
|
|
35
35
|
temperature?: number;
|
|
36
36
|
maxToolIterations?: number;
|
|
37
37
|
};
|
|
38
|
+
context?: {
|
|
39
|
+
bootstrap?: {
|
|
40
|
+
files?: string[];
|
|
41
|
+
minimalFiles?: string[];
|
|
42
|
+
heartbeatFiles?: string[];
|
|
43
|
+
perFileChars?: number;
|
|
44
|
+
totalChars?: number;
|
|
45
|
+
};
|
|
46
|
+
memory?: {
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
maxChars?: number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
38
51
|
};
|
|
39
52
|
providers: Record<string, ProviderConfigView>;
|
|
40
53
|
channels: Record<string, Record<string, unknown>>;
|
|
@@ -67,6 +80,25 @@ export type ConfigMetaView = {
|
|
|
67
80
|
channels: ChannelSpecView[];
|
|
68
81
|
};
|
|
69
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
|
+
|
|
70
102
|
export type FeishuProbeView = {
|
|
71
103
|
appId: string;
|
|
72
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">{
|
|
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
|
-
|
|
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
|
-
{
|
|
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=
|
|
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
|
-
|
|
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
|
-
{
|
|
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">
|
|
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=
|
|
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">
|
|
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">
|
|
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=
|
|
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">
|
|
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">
|
|
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={
|
|
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={
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
{
|
|
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
|
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
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
|
+
}
|