@jupyternaut/persona 0.0.0 → 0.20.0
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/lib/chat-commands/mention.d.ts +9 -0
- package/lib/chat-commands/mention.js +30 -0
- package/lib/completion/completion-provider.d.ts +86 -0
- package/lib/completion/completion-provider.js +246 -0
- package/lib/completion/index.d.ts +2 -0
- package/lib/completion/index.js +1 -0
- package/lib/components/completion-status.d.ts +26 -0
- package/lib/components/completion-status.js +52 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +1 -0
- package/lib/diff-manager.d.ts +25 -0
- package/lib/diff-manager.js +60 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +522 -0
- package/lib/models/settings-model.d.ts +36 -0
- package/lib/models/settings-model.js +356 -0
- package/lib/persona-registry.d.ts +15 -0
- package/lib/persona-registry.js +29 -0
- package/lib/persona.d.ts +66 -0
- package/lib/persona.js +414 -0
- package/lib/process-attachments.d.ts +5 -0
- package/lib/process-attachments.js +287 -0
- package/lib/tokens.d.ts +101 -0
- package/lib/tokens.js +20 -0
- package/lib/widgets/ai-settings.d.ts +54 -0
- package/lib/widgets/ai-settings.js +572 -0
- package/lib/widgets/provider-config-dialog.d.ts +16 -0
- package/lib/widgets/provider-config-dialog.js +384 -0
- package/package.json +111 -7
- package/schema/settings-model.json +287 -0
- package/src/chat-commands/mention.tsx +46 -0
- package/src/completion/completion-provider.ts +350 -0
- package/src/completion/index.ts +1 -0
- package/src/components/completion-status.tsx +93 -0
- package/src/components/index.ts +1 -0
- package/src/diff-manager.ts +81 -0
- package/src/index.ts +710 -0
- package/src/models/settings-model.ts +415 -0
- package/src/persona-registry.ts +46 -0
- package/src/persona.ts +610 -0
- package/src/process-attachments.ts +369 -0
- package/src/tokens.ts +121 -0
- package/src/widgets/ai-settings.tsx +1308 -0
- package/src/widgets/provider-config-dialog.tsx +997 -0
- package/style/base.css +14 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/README.md +0 -3
- package/index.js +0 -1
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
import { getProviderModelInfo } from '@jupyternaut/agent';
|
|
2
|
+
import type {
|
|
3
|
+
IProviderConfig,
|
|
4
|
+
IProviderParameters,
|
|
5
|
+
IProviderRegistry,
|
|
6
|
+
IProviderToolCapabilities
|
|
7
|
+
} from '@jupyternaut/agent';
|
|
8
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
9
|
+
import ExpandMore from '@mui/icons-material/ExpandMore';
|
|
10
|
+
import Delete from '@mui/icons-material/Delete';
|
|
11
|
+
import Visibility from '@mui/icons-material/Visibility';
|
|
12
|
+
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|
13
|
+
import {
|
|
14
|
+
Accordion,
|
|
15
|
+
AccordionDetails,
|
|
16
|
+
AccordionSummary,
|
|
17
|
+
Autocomplete,
|
|
18
|
+
Box,
|
|
19
|
+
Button,
|
|
20
|
+
Chip,
|
|
21
|
+
Dialog,
|
|
22
|
+
DialogActions,
|
|
23
|
+
DialogContent,
|
|
24
|
+
DialogTitle,
|
|
25
|
+
FormControl,
|
|
26
|
+
FormControlLabel,
|
|
27
|
+
IconButton,
|
|
28
|
+
InputAdornment,
|
|
29
|
+
InputLabel,
|
|
30
|
+
List,
|
|
31
|
+
ListItem,
|
|
32
|
+
ListItemText,
|
|
33
|
+
MenuItem,
|
|
34
|
+
Select,
|
|
35
|
+
Slider,
|
|
36
|
+
Switch,
|
|
37
|
+
TextField,
|
|
38
|
+
Typography
|
|
39
|
+
} from '@mui/material';
|
|
40
|
+
import React from 'react';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default parameter values for provider configuration
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_TEMPERATURE = 0.7;
|
|
46
|
+
const DEFAULT_MAX_TURNS = 25;
|
|
47
|
+
type DomainSection = 'webSearch' | 'webFetch';
|
|
48
|
+
type DomainKey = 'allowedDomains' | 'blockedDomains';
|
|
49
|
+
type DomainFieldId = `${DomainSection}.${DomainKey}`;
|
|
50
|
+
|
|
51
|
+
const DOMAIN_FIELD_MAP: Record<
|
|
52
|
+
DomainFieldId,
|
|
53
|
+
{ section: DomainSection; key: DomainKey }
|
|
54
|
+
> = {
|
|
55
|
+
'webSearch.allowedDomains': {
|
|
56
|
+
section: 'webSearch',
|
|
57
|
+
key: 'allowedDomains'
|
|
58
|
+
},
|
|
59
|
+
'webSearch.blockedDomains': {
|
|
60
|
+
section: 'webSearch',
|
|
61
|
+
key: 'blockedDomains'
|
|
62
|
+
},
|
|
63
|
+
'webFetch.allowedDomains': {
|
|
64
|
+
section: 'webFetch',
|
|
65
|
+
key: 'allowedDomains'
|
|
66
|
+
},
|
|
67
|
+
'webFetch.blockedDomains': {
|
|
68
|
+
section: 'webFetch',
|
|
69
|
+
key: 'blockedDomains'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function createEmptyDomainInputs(): Record<DomainFieldId, string> {
|
|
74
|
+
return {
|
|
75
|
+
'webSearch.allowedDomains': '',
|
|
76
|
+
'webSearch.blockedDomains': '',
|
|
77
|
+
'webFetch.allowedDomains': '',
|
|
78
|
+
'webFetch.blockedDomains': ''
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toRecord(value: unknown): Record<string, any> {
|
|
83
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
84
|
+
return value as Record<string, any>;
|
|
85
|
+
}
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toStringArray(value: unknown): string[] {
|
|
90
|
+
if (!Array.isArray(value)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return value.filter((item): item is string => typeof item === 'string');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sanitizeCustomSettingsForProvider(
|
|
97
|
+
customSettings: Record<string, any>,
|
|
98
|
+
capabilities?: IProviderToolCapabilities
|
|
99
|
+
): Record<string, any> {
|
|
100
|
+
const result: Record<string, any> = { ...customSettings };
|
|
101
|
+
const webSearch = toRecord(customSettings.webSearch);
|
|
102
|
+
const webFetch = toRecord(customSettings.webFetch);
|
|
103
|
+
const supportsWebSearch = !!capabilities?.webSearch;
|
|
104
|
+
const supportsWebFetch = !!capabilities?.webFetch;
|
|
105
|
+
|
|
106
|
+
if (supportsWebSearch && webSearch.enabled === true) {
|
|
107
|
+
result.webSearch = webSearch;
|
|
108
|
+
} else {
|
|
109
|
+
delete result.webSearch;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (supportsWebFetch && webFetch.enabled === true) {
|
|
113
|
+
result.webFetch = webFetch;
|
|
114
|
+
} else {
|
|
115
|
+
delete result.webFetch;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface IProviderConfigDialogProps {
|
|
122
|
+
open: boolean;
|
|
123
|
+
onClose: () => void;
|
|
124
|
+
onSave: (config: Omit<IProviderConfig, 'id'>) => void;
|
|
125
|
+
initialConfig?: IProviderConfig;
|
|
126
|
+
mode: 'add' | 'edit';
|
|
127
|
+
providerRegistry: IProviderRegistry;
|
|
128
|
+
handleSecretField: (
|
|
129
|
+
input: HTMLInputElement,
|
|
130
|
+
provider: string,
|
|
131
|
+
fieldName: string
|
|
132
|
+
) => Promise<void>;
|
|
133
|
+
trans: TranslationBundle;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
|
|
137
|
+
open,
|
|
138
|
+
onClose,
|
|
139
|
+
onSave,
|
|
140
|
+
initialConfig,
|
|
141
|
+
mode,
|
|
142
|
+
providerRegistry,
|
|
143
|
+
handleSecretField,
|
|
144
|
+
trans
|
|
145
|
+
}) => {
|
|
146
|
+
const [name, setName] = React.useState(initialConfig?.name || '');
|
|
147
|
+
const [provider, setProvider] = React.useState(
|
|
148
|
+
initialConfig?.provider || 'anthropic'
|
|
149
|
+
);
|
|
150
|
+
const [model, setModel] = React.useState(initialConfig?.model || '');
|
|
151
|
+
const [apiKey, setApiKey] = React.useState(initialConfig?.apiKey || '');
|
|
152
|
+
const [baseURL, setBaseURL] = React.useState(initialConfig?.baseURL || '');
|
|
153
|
+
const [showApiKey, setShowApiKey] = React.useState(false);
|
|
154
|
+
const [customSettings, setCustomSettings] = React.useState<
|
|
155
|
+
Record<string, any>
|
|
156
|
+
>(initialConfig?.customSettings || {});
|
|
157
|
+
const [domainInputs, setDomainInputs] = React.useState<
|
|
158
|
+
Record<DomainFieldId, string>
|
|
159
|
+
>(createEmptyDomainInputs());
|
|
160
|
+
|
|
161
|
+
const [parameters, setParameters] = React.useState<IProviderParameters>(
|
|
162
|
+
initialConfig?.parameters || {}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const [expandedAdvanced, setExpandedAdvanced] = React.useState(false);
|
|
166
|
+
const selectedProviderInfo = React.useMemo(
|
|
167
|
+
() => providerRegistry.getProviderInfo(provider),
|
|
168
|
+
[providerRegistry, provider]
|
|
169
|
+
);
|
|
170
|
+
const providerToolCapabilities =
|
|
171
|
+
selectedProviderInfo?.providerToolCapabilities;
|
|
172
|
+
const selectedModelInfo = React.useMemo(
|
|
173
|
+
() => getProviderModelInfo(selectedProviderInfo, model),
|
|
174
|
+
[selectedProviderInfo, model]
|
|
175
|
+
);
|
|
176
|
+
const webSearchImplementation =
|
|
177
|
+
providerToolCapabilities?.webSearch?.implementation;
|
|
178
|
+
const supportsWebSearch = !!providerToolCapabilities?.webSearch;
|
|
179
|
+
const supportsWebFetch = !!providerToolCapabilities?.webFetch;
|
|
180
|
+
const webSearchSettings = React.useMemo(
|
|
181
|
+
() => toRecord(customSettings.webSearch),
|
|
182
|
+
[customSettings]
|
|
183
|
+
);
|
|
184
|
+
const webFetchSettings = React.useMemo(
|
|
185
|
+
() => toRecord(customSettings.webFetch),
|
|
186
|
+
[customSettings]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Get provider options from registry
|
|
190
|
+
const providerOptions = React.useMemo(() => {
|
|
191
|
+
const providers = providerRegistry.providers;
|
|
192
|
+
return Object.keys(providers).map(id => {
|
|
193
|
+
const info = providers[id];
|
|
194
|
+
return {
|
|
195
|
+
value: id,
|
|
196
|
+
label: info.name,
|
|
197
|
+
models: info.defaultModels,
|
|
198
|
+
apiKeyRequirement: info.apiKeyRequirement,
|
|
199
|
+
supportsBaseURL: info.supportsBaseURL,
|
|
200
|
+
description: info.description,
|
|
201
|
+
baseUrls: info.baseUrls
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
}, [providerRegistry]);
|
|
205
|
+
|
|
206
|
+
const selectedProvider = providerOptions.find(p => p.value === provider);
|
|
207
|
+
|
|
208
|
+
React.useEffect(() => {
|
|
209
|
+
if (open) {
|
|
210
|
+
// Reset form when dialog opens
|
|
211
|
+
const initialProvider = initialConfig?.provider || 'anthropic';
|
|
212
|
+
const initialProviderInfo =
|
|
213
|
+
providerRegistry.getProviderInfo(initialProvider);
|
|
214
|
+
setName(initialConfig?.name || '');
|
|
215
|
+
setProvider(initialProvider);
|
|
216
|
+
setModel(
|
|
217
|
+
initialConfig?.model || initialProviderInfo?.defaultModels[0] || ''
|
|
218
|
+
);
|
|
219
|
+
setApiKey(initialConfig?.apiKey || '');
|
|
220
|
+
setBaseURL(initialConfig?.baseURL || '');
|
|
221
|
+
setParameters(initialConfig?.parameters || {});
|
|
222
|
+
setCustomSettings(initialConfig?.customSettings || {});
|
|
223
|
+
setDomainInputs(createEmptyDomainInputs());
|
|
224
|
+
setShowApiKey(false);
|
|
225
|
+
setExpandedAdvanced(false);
|
|
226
|
+
} else {
|
|
227
|
+
// Reset expanded state when dialog closes
|
|
228
|
+
setDomainInputs(createEmptyDomainInputs());
|
|
229
|
+
setExpandedAdvanced(false);
|
|
230
|
+
}
|
|
231
|
+
}, [open, initialConfig, providerRegistry]);
|
|
232
|
+
|
|
233
|
+
const handleRef = React.useCallback(
|
|
234
|
+
(node: HTMLInputElement | null) => {
|
|
235
|
+
if (open && node) {
|
|
236
|
+
handleSecretField(node, provider, 'apiKey');
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
[provider, handleSecretField, open]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const handleProviderChange = React.useCallback(
|
|
243
|
+
(newProvider: IProviderConfig['provider']) => {
|
|
244
|
+
const newProviderInfo = providerRegistry.getProviderInfo(newProvider);
|
|
245
|
+
setProvider(newProvider);
|
|
246
|
+
setModel(newProviderInfo?.defaultModels[0] || '');
|
|
247
|
+
},
|
|
248
|
+
[providerRegistry]
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const updateCustomSetting = React.useCallback(
|
|
252
|
+
(section: 'webSearch' | 'webFetch', key: string, value: unknown) => {
|
|
253
|
+
setCustomSettings(prev => {
|
|
254
|
+
const next = { ...prev };
|
|
255
|
+
const sectionSettings = { ...toRecord(next[section]) };
|
|
256
|
+
const shouldDelete =
|
|
257
|
+
value === undefined ||
|
|
258
|
+
value === null ||
|
|
259
|
+
value === '' ||
|
|
260
|
+
(Array.isArray(value) && value.length === 0);
|
|
261
|
+
|
|
262
|
+
if (shouldDelete) {
|
|
263
|
+
delete sectionSettings[key];
|
|
264
|
+
} else {
|
|
265
|
+
sectionSettings[key] = value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (Object.keys(sectionSettings).length === 0) {
|
|
269
|
+
delete next[section];
|
|
270
|
+
} else {
|
|
271
|
+
next[section] = sectionSettings;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return next;
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
[]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const addDomainValue = React.useCallback(
|
|
281
|
+
(fieldId: DomainFieldId) => {
|
|
282
|
+
const valueToAdd = domainInputs[fieldId].trim();
|
|
283
|
+
if (!valueToAdd) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { section, key } = DOMAIN_FIELD_MAP[fieldId];
|
|
288
|
+
const currentValues = toStringArray(
|
|
289
|
+
toRecord(customSettings[section])[key]
|
|
290
|
+
);
|
|
291
|
+
if (currentValues.includes(valueToAdd)) {
|
|
292
|
+
setDomainInputs(prev => ({
|
|
293
|
+
...prev,
|
|
294
|
+
[fieldId]: ''
|
|
295
|
+
}));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const nextValues = [...currentValues, valueToAdd];
|
|
299
|
+
|
|
300
|
+
updateCustomSetting(section, key, nextValues);
|
|
301
|
+
setDomainInputs(prev => ({
|
|
302
|
+
...prev,
|
|
303
|
+
[fieldId]: ''
|
|
304
|
+
}));
|
|
305
|
+
},
|
|
306
|
+
[customSettings, domainInputs, updateCustomSetting]
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const removeDomainValue = React.useCallback(
|
|
310
|
+
(fieldId: DomainFieldId, valueToRemove: string) => {
|
|
311
|
+
const { section, key } = DOMAIN_FIELD_MAP[fieldId];
|
|
312
|
+
const currentValues = toStringArray(
|
|
313
|
+
toRecord(customSettings[section])[key]
|
|
314
|
+
);
|
|
315
|
+
const nextValues = currentValues.filter(value => value !== valueToRemove);
|
|
316
|
+
updateCustomSetting(
|
|
317
|
+
section,
|
|
318
|
+
key,
|
|
319
|
+
nextValues.length > 0 ? nextValues : undefined
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
[customSettings, updateCustomSetting]
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const renderDomainList = React.useCallback(
|
|
326
|
+
(
|
|
327
|
+
fieldId: DomainFieldId,
|
|
328
|
+
label: string,
|
|
329
|
+
placeholder: string,
|
|
330
|
+
values: unknown
|
|
331
|
+
) => {
|
|
332
|
+
const domainValues = toStringArray(values);
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<Box>
|
|
336
|
+
<Typography variant="body2" gutterBottom>
|
|
337
|
+
{label}
|
|
338
|
+
</Typography>
|
|
339
|
+
<List
|
|
340
|
+
dense
|
|
341
|
+
sx={{
|
|
342
|
+
mb: 1,
|
|
343
|
+
maxHeight: 160,
|
|
344
|
+
overflow: 'auto',
|
|
345
|
+
border: 1,
|
|
346
|
+
borderColor: 'divider',
|
|
347
|
+
borderRadius: 1
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
{domainValues.length === 0 ? (
|
|
351
|
+
<ListItem>
|
|
352
|
+
<ListItemText
|
|
353
|
+
secondary={trans.__('No domains added.')}
|
|
354
|
+
slotProps={{
|
|
355
|
+
secondary: {
|
|
356
|
+
color: 'text.secondary'
|
|
357
|
+
}
|
|
358
|
+
}}
|
|
359
|
+
/>
|
|
360
|
+
</ListItem>
|
|
361
|
+
) : (
|
|
362
|
+
domainValues.map(value => (
|
|
363
|
+
<ListItem
|
|
364
|
+
key={value}
|
|
365
|
+
secondaryAction={
|
|
366
|
+
<IconButton
|
|
367
|
+
onClick={() => removeDomainValue(fieldId, value)}
|
|
368
|
+
size="small"
|
|
369
|
+
>
|
|
370
|
+
<Delete fontSize="small" />
|
|
371
|
+
</IconButton>
|
|
372
|
+
}
|
|
373
|
+
>
|
|
374
|
+
<ListItemText primary={value} />
|
|
375
|
+
</ListItem>
|
|
376
|
+
))
|
|
377
|
+
)}
|
|
378
|
+
</List>
|
|
379
|
+
<TextField
|
|
380
|
+
fullWidth
|
|
381
|
+
size="small"
|
|
382
|
+
label={trans.__('Add Domain')}
|
|
383
|
+
value={domainInputs[fieldId]}
|
|
384
|
+
onChange={e =>
|
|
385
|
+
setDomainInputs(prev => ({
|
|
386
|
+
...prev,
|
|
387
|
+
[fieldId]: e.target.value
|
|
388
|
+
}))
|
|
389
|
+
}
|
|
390
|
+
onKeyDown={e => {
|
|
391
|
+
if (e.key === 'Enter') {
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
addDomainValue(fieldId);
|
|
394
|
+
}
|
|
395
|
+
}}
|
|
396
|
+
placeholder={placeholder}
|
|
397
|
+
helperText={trans.__('Press Enter to add one domain.')}
|
|
398
|
+
/>
|
|
399
|
+
</Box>
|
|
400
|
+
);
|
|
401
|
+
},
|
|
402
|
+
[addDomainValue, domainInputs, removeDomainValue, trans]
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const handleSave = () => {
|
|
406
|
+
if (!name.trim() || !provider || !model) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Only include parameters if at least one is set
|
|
411
|
+
const hasParameters = Object.keys(parameters).some(
|
|
412
|
+
key => parameters[key as keyof IProviderParameters] !== undefined
|
|
413
|
+
);
|
|
414
|
+
const sanitizedCustomSettings = sanitizeCustomSettingsForProvider(
|
|
415
|
+
customSettings,
|
|
416
|
+
providerToolCapabilities
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const config: Omit<IProviderConfig, 'id'> = {
|
|
420
|
+
name: name.trim(),
|
|
421
|
+
provider: provider as IProviderConfig['provider'],
|
|
422
|
+
model,
|
|
423
|
+
...(apiKey && { apiKey }),
|
|
424
|
+
...(baseURL && { baseURL }),
|
|
425
|
+
...(hasParameters && { parameters }),
|
|
426
|
+
...(Object.keys(sanitizedCustomSettings).length > 0 && {
|
|
427
|
+
customSettings: sanitizedCustomSettings
|
|
428
|
+
})
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
onSave(config);
|
|
432
|
+
onClose();
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const isValid =
|
|
436
|
+
name.trim() &&
|
|
437
|
+
provider &&
|
|
438
|
+
model &&
|
|
439
|
+
(selectedProvider?.apiKeyRequirement !== 'required' || apiKey);
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
|
443
|
+
<DialogTitle>
|
|
444
|
+
{mode === 'add'
|
|
445
|
+
? trans.__('Add New Provider')
|
|
446
|
+
: trans.__('Edit Provider')}
|
|
447
|
+
</DialogTitle>
|
|
448
|
+
<DialogContent>
|
|
449
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
|
|
450
|
+
<TextField
|
|
451
|
+
fullWidth
|
|
452
|
+
label={trans.__('Provider Name')}
|
|
453
|
+
value={name}
|
|
454
|
+
onChange={e => setName(e.target.value)}
|
|
455
|
+
placeholder={trans.__('e.g., My Anthropic Config, Work Provider')}
|
|
456
|
+
helperText={trans.__(
|
|
457
|
+
'A friendly name to identify this provider configuration'
|
|
458
|
+
)}
|
|
459
|
+
required
|
|
460
|
+
/>
|
|
461
|
+
|
|
462
|
+
<FormControl fullWidth required>
|
|
463
|
+
<InputLabel>{trans.__('Provider Type')}</InputLabel>
|
|
464
|
+
<Select
|
|
465
|
+
value={provider}
|
|
466
|
+
label={trans.__('Provider Type')}
|
|
467
|
+
onChange={e =>
|
|
468
|
+
handleProviderChange(
|
|
469
|
+
e.target.value as IProviderConfig['provider']
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
>
|
|
473
|
+
{providerOptions.map(option => (
|
|
474
|
+
<MenuItem key={option.value} value={option.value}>
|
|
475
|
+
<Box>
|
|
476
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
477
|
+
{option.label}
|
|
478
|
+
{option.apiKeyRequirement === 'required' && (
|
|
479
|
+
<Chip
|
|
480
|
+
size="small"
|
|
481
|
+
label={trans.__('API Key')}
|
|
482
|
+
color="default"
|
|
483
|
+
variant="outlined"
|
|
484
|
+
/>
|
|
485
|
+
)}
|
|
486
|
+
</Box>
|
|
487
|
+
{option.description && (
|
|
488
|
+
<Typography variant="caption" color="text.secondary">
|
|
489
|
+
{option.description}
|
|
490
|
+
</Typography>
|
|
491
|
+
)}
|
|
492
|
+
</Box>
|
|
493
|
+
</MenuItem>
|
|
494
|
+
))}
|
|
495
|
+
</Select>
|
|
496
|
+
</FormControl>
|
|
497
|
+
|
|
498
|
+
<Autocomplete
|
|
499
|
+
freeSolo
|
|
500
|
+
fullWidth
|
|
501
|
+
options={selectedProvider?.models ?? []}
|
|
502
|
+
value={model}
|
|
503
|
+
onChange={(_, value) => {
|
|
504
|
+
setModel(typeof value === 'string' ? value : '');
|
|
505
|
+
}}
|
|
506
|
+
inputValue={model}
|
|
507
|
+
onInputChange={(_, value) => {
|
|
508
|
+
setModel(value);
|
|
509
|
+
}}
|
|
510
|
+
renderInput={params => (
|
|
511
|
+
<TextField
|
|
512
|
+
{...params}
|
|
513
|
+
fullWidth
|
|
514
|
+
label={trans.__('Model')}
|
|
515
|
+
placeholder={trans.__('Select or type a model ID')}
|
|
516
|
+
required
|
|
517
|
+
helperText={trans.__(
|
|
518
|
+
'Choose from the list or enter a custom model ID'
|
|
519
|
+
)}
|
|
520
|
+
/>
|
|
521
|
+
)}
|
|
522
|
+
clearOnBlur={false}
|
|
523
|
+
/>
|
|
524
|
+
|
|
525
|
+
{selectedProvider &&
|
|
526
|
+
selectedProvider?.apiKeyRequirement !== 'none' && (
|
|
527
|
+
<TextField
|
|
528
|
+
fullWidth
|
|
529
|
+
inputRef={handleRef}
|
|
530
|
+
label={
|
|
531
|
+
selectedProvider?.apiKeyRequirement === 'required'
|
|
532
|
+
? trans.__('API Key')
|
|
533
|
+
: trans.__('API Key (Optional)')
|
|
534
|
+
}
|
|
535
|
+
type={showApiKey ? 'text' : 'password'}
|
|
536
|
+
value={apiKey}
|
|
537
|
+
onChange={e => setApiKey(e.target.value)}
|
|
538
|
+
placeholder={trans.__('Enter your API key...')}
|
|
539
|
+
required={selectedProvider?.apiKeyRequirement === 'required'}
|
|
540
|
+
InputProps={{
|
|
541
|
+
endAdornment: (
|
|
542
|
+
<InputAdornment position="end">
|
|
543
|
+
<IconButton
|
|
544
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
|
545
|
+
edge="end"
|
|
546
|
+
>
|
|
547
|
+
{showApiKey ? <VisibilityOff /> : <Visibility />}
|
|
548
|
+
</IconButton>
|
|
549
|
+
</InputAdornment>
|
|
550
|
+
)
|
|
551
|
+
}}
|
|
552
|
+
/>
|
|
553
|
+
)}
|
|
554
|
+
|
|
555
|
+
{selectedProvider?.supportsBaseURL && (
|
|
556
|
+
<Autocomplete
|
|
557
|
+
freeSolo
|
|
558
|
+
fullWidth
|
|
559
|
+
options={(selectedProvider.baseUrls ?? []).map(
|
|
560
|
+
option => option.url
|
|
561
|
+
)}
|
|
562
|
+
value={baseURL || ''}
|
|
563
|
+
onChange={(_, value) => {
|
|
564
|
+
if (value && typeof value === 'string') {
|
|
565
|
+
setBaseURL(value);
|
|
566
|
+
}
|
|
567
|
+
}}
|
|
568
|
+
inputValue={baseURL || ''}
|
|
569
|
+
renderOption={(props, option) => {
|
|
570
|
+
const urlOption = (selectedProvider.baseUrls ?? []).find(
|
|
571
|
+
u => u.url === option
|
|
572
|
+
);
|
|
573
|
+
return (
|
|
574
|
+
<Box component="li" {...props} key={option}>
|
|
575
|
+
<Box>
|
|
576
|
+
<Typography variant="body2">{option}</Typography>
|
|
577
|
+
{urlOption?.description && (
|
|
578
|
+
<Typography variant="caption" color="text.secondary">
|
|
579
|
+
{urlOption.description}
|
|
580
|
+
</Typography>
|
|
581
|
+
)}
|
|
582
|
+
</Box>
|
|
583
|
+
</Box>
|
|
584
|
+
);
|
|
585
|
+
}}
|
|
586
|
+
renderInput={params => (
|
|
587
|
+
<TextField
|
|
588
|
+
{...params}
|
|
589
|
+
fullWidth
|
|
590
|
+
label={trans.__('Base URL')}
|
|
591
|
+
placeholder="https://api.example.com/v1"
|
|
592
|
+
onChange={e => setBaseURL(e.target.value)}
|
|
593
|
+
/>
|
|
594
|
+
)}
|
|
595
|
+
clearOnBlur={false}
|
|
596
|
+
/>
|
|
597
|
+
)}
|
|
598
|
+
|
|
599
|
+
{/* Advanced Settings Section */}
|
|
600
|
+
<Accordion
|
|
601
|
+
expanded={expandedAdvanced}
|
|
602
|
+
onChange={(_, isExpanded) => setExpandedAdvanced(isExpanded)}
|
|
603
|
+
sx={{
|
|
604
|
+
mt: 2,
|
|
605
|
+
bgcolor: 'transparent',
|
|
606
|
+
boxShadow: 'none',
|
|
607
|
+
border: 1,
|
|
608
|
+
borderColor: 'divider',
|
|
609
|
+
borderRadius: 1
|
|
610
|
+
}}
|
|
611
|
+
>
|
|
612
|
+
<AccordionSummary expandIcon={<ExpandMore />}>
|
|
613
|
+
<Typography variant="subtitle1" fontWeight="medium">
|
|
614
|
+
{trans.__('Advanced Settings')}
|
|
615
|
+
</Typography>
|
|
616
|
+
</AccordionSummary>
|
|
617
|
+
<AccordionDetails sx={{ bgcolor: 'transparent' }}>
|
|
618
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
619
|
+
<Box>
|
|
620
|
+
<Typography gutterBottom>
|
|
621
|
+
{trans.__(
|
|
622
|
+
'Temperature: %1',
|
|
623
|
+
parameters.temperature ?? trans.__('Default')
|
|
624
|
+
)}
|
|
625
|
+
</Typography>
|
|
626
|
+
<Slider
|
|
627
|
+
value={parameters.temperature ?? DEFAULT_TEMPERATURE}
|
|
628
|
+
onChange={(_, value) =>
|
|
629
|
+
setParameters({
|
|
630
|
+
...parameters,
|
|
631
|
+
temperature: value as number
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
min={0}
|
|
635
|
+
max={2}
|
|
636
|
+
step={0.1}
|
|
637
|
+
valueLabelDisplay="auto"
|
|
638
|
+
/>
|
|
639
|
+
<Typography variant="caption" color="text.secondary">
|
|
640
|
+
{trans.__(
|
|
641
|
+
'Temperature for the model (lower values are more deterministic)'
|
|
642
|
+
)}
|
|
643
|
+
</Typography>
|
|
644
|
+
</Box>
|
|
645
|
+
|
|
646
|
+
<TextField
|
|
647
|
+
fullWidth
|
|
648
|
+
label={trans.__('Max Tokens (Optional)')}
|
|
649
|
+
type="number"
|
|
650
|
+
value={parameters.maxOutputTokens ?? ''}
|
|
651
|
+
onChange={e =>
|
|
652
|
+
setParameters({
|
|
653
|
+
...parameters,
|
|
654
|
+
maxOutputTokens: e.target.value
|
|
655
|
+
? Number(e.target.value)
|
|
656
|
+
: undefined
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
placeholder={trans.__('Leave empty for provider default')}
|
|
660
|
+
helperText={trans.__('Maximum length of AI responses')}
|
|
661
|
+
slotProps={{ htmlInput: { min: 1 } }}
|
|
662
|
+
/>
|
|
663
|
+
|
|
664
|
+
<TextField
|
|
665
|
+
fullWidth
|
|
666
|
+
label={trans.__('Max Turns (Optional)')}
|
|
667
|
+
type="number"
|
|
668
|
+
value={parameters.maxTurns ?? ''}
|
|
669
|
+
onChange={e =>
|
|
670
|
+
setParameters({
|
|
671
|
+
...parameters,
|
|
672
|
+
maxTurns: e.target.value
|
|
673
|
+
? Number(e.target.value)
|
|
674
|
+
: undefined
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
placeholder={trans.__('Default: %1', DEFAULT_MAX_TURNS)}
|
|
678
|
+
helperText={trans.__(
|
|
679
|
+
'Maximum number of tool execution turns'
|
|
680
|
+
)}
|
|
681
|
+
slotProps={{ htmlInput: { min: 1, max: 100 } }}
|
|
682
|
+
/>
|
|
683
|
+
|
|
684
|
+
<TextField
|
|
685
|
+
fullWidth
|
|
686
|
+
label={trans.__('Context Window (Optional)')}
|
|
687
|
+
type="number"
|
|
688
|
+
value={parameters.contextWindow ?? ''}
|
|
689
|
+
onChange={e =>
|
|
690
|
+
setParameters({
|
|
691
|
+
...parameters,
|
|
692
|
+
contextWindow: e.target.value
|
|
693
|
+
? Number(e.target.value)
|
|
694
|
+
: undefined
|
|
695
|
+
})
|
|
696
|
+
}
|
|
697
|
+
placeholder={
|
|
698
|
+
selectedModelInfo?.contextWindow !== undefined
|
|
699
|
+
? trans.__(
|
|
700
|
+
'Default: %1',
|
|
701
|
+
selectedModelInfo.contextWindow.toLocaleString()
|
|
702
|
+
)
|
|
703
|
+
: trans.__('e.g., 128000')
|
|
704
|
+
}
|
|
705
|
+
helperText={
|
|
706
|
+
selectedModelInfo?.contextWindow !== undefined &&
|
|
707
|
+
parameters.contextWindow === undefined
|
|
708
|
+
? trans.__(
|
|
709
|
+
'Using provider metadata default of %1 tokens for this model unless you override it here.',
|
|
710
|
+
selectedModelInfo.contextWindow.toLocaleString()
|
|
711
|
+
)
|
|
712
|
+
: trans.__(
|
|
713
|
+
'Model context window size in tokens (used for context usage estimation)'
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
slotProps={{ htmlInput: { min: 1 } }}
|
|
717
|
+
/>
|
|
718
|
+
|
|
719
|
+
<Typography
|
|
720
|
+
variant="body2"
|
|
721
|
+
color="text.secondary"
|
|
722
|
+
sx={{ mt: 2, mb: 1 }}
|
|
723
|
+
>
|
|
724
|
+
{trans.__('Completion Options')}
|
|
725
|
+
</Typography>
|
|
726
|
+
|
|
727
|
+
<FormControlLabel
|
|
728
|
+
control={
|
|
729
|
+
<Switch
|
|
730
|
+
checked={parameters.supportsFillInMiddle ?? false}
|
|
731
|
+
onChange={e =>
|
|
732
|
+
setParameters({
|
|
733
|
+
...parameters,
|
|
734
|
+
supportsFillInMiddle: e.target.checked
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
/>
|
|
738
|
+
}
|
|
739
|
+
label={trans.__('Fill-in-the-middle support')}
|
|
740
|
+
/>
|
|
741
|
+
|
|
742
|
+
<FormControlLabel
|
|
743
|
+
control={
|
|
744
|
+
<Switch
|
|
745
|
+
checked={parameters.useFilterText ?? false}
|
|
746
|
+
onChange={e =>
|
|
747
|
+
setParameters({
|
|
748
|
+
...parameters,
|
|
749
|
+
useFilterText: e.target.checked
|
|
750
|
+
})
|
|
751
|
+
}
|
|
752
|
+
/>
|
|
753
|
+
}
|
|
754
|
+
label={trans.__('Use filter text')}
|
|
755
|
+
/>
|
|
756
|
+
|
|
757
|
+
{(supportsWebSearch || supportsWebFetch) && (
|
|
758
|
+
<>
|
|
759
|
+
<Typography
|
|
760
|
+
variant="body2"
|
|
761
|
+
color="text.secondary"
|
|
762
|
+
sx={{ mt: 2, mb: 1 }}
|
|
763
|
+
>
|
|
764
|
+
{trans.__('Provider Web Tools')}
|
|
765
|
+
</Typography>
|
|
766
|
+
|
|
767
|
+
{supportsWebSearch && (
|
|
768
|
+
<>
|
|
769
|
+
<FormControlLabel
|
|
770
|
+
control={
|
|
771
|
+
<Switch
|
|
772
|
+
checked={webSearchSettings.enabled === true}
|
|
773
|
+
onChange={e =>
|
|
774
|
+
updateCustomSetting(
|
|
775
|
+
'webSearch',
|
|
776
|
+
'enabled',
|
|
777
|
+
e.target.checked
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
/>
|
|
781
|
+
}
|
|
782
|
+
label={trans.__('Enable Web Search')}
|
|
783
|
+
/>
|
|
784
|
+
|
|
785
|
+
{webSearchSettings.enabled === true && (
|
|
786
|
+
<Box
|
|
787
|
+
sx={{
|
|
788
|
+
pl: 2,
|
|
789
|
+
borderLeft: 2,
|
|
790
|
+
borderColor: 'divider',
|
|
791
|
+
display: 'flex',
|
|
792
|
+
flexDirection: 'column',
|
|
793
|
+
gap: 1.5
|
|
794
|
+
}}
|
|
795
|
+
>
|
|
796
|
+
{(webSearchImplementation === 'openai' ||
|
|
797
|
+
webSearchImplementation === 'anthropic') &&
|
|
798
|
+
renderDomainList(
|
|
799
|
+
'webSearch.allowedDomains',
|
|
800
|
+
trans.__('Allowed Domains'),
|
|
801
|
+
trans.__('example.com'),
|
|
802
|
+
webSearchSettings.allowedDomains
|
|
803
|
+
)}
|
|
804
|
+
|
|
805
|
+
{webSearchImplementation === 'openai' && (
|
|
806
|
+
<>
|
|
807
|
+
<FormControl fullWidth>
|
|
808
|
+
<InputLabel>
|
|
809
|
+
{trans.__('Search Context Size')}
|
|
810
|
+
</InputLabel>
|
|
811
|
+
<Select
|
|
812
|
+
value={
|
|
813
|
+
webSearchSettings.searchContextSize ??
|
|
814
|
+
'medium'
|
|
815
|
+
}
|
|
816
|
+
label={trans.__('Search Context Size')}
|
|
817
|
+
onChange={e =>
|
|
818
|
+
updateCustomSetting(
|
|
819
|
+
'webSearch',
|
|
820
|
+
'searchContextSize',
|
|
821
|
+
e.target.value
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
>
|
|
825
|
+
<MenuItem value="low">
|
|
826
|
+
{trans.__('Low')}
|
|
827
|
+
</MenuItem>
|
|
828
|
+
<MenuItem value="medium">
|
|
829
|
+
{trans.__('Medium')}
|
|
830
|
+
</MenuItem>
|
|
831
|
+
<MenuItem value="high">
|
|
832
|
+
{trans.__('High')}
|
|
833
|
+
</MenuItem>
|
|
834
|
+
</Select>
|
|
835
|
+
</FormControl>
|
|
836
|
+
<FormControlLabel
|
|
837
|
+
control={
|
|
838
|
+
<Switch
|
|
839
|
+
checked={
|
|
840
|
+
webSearchSettings.externalWebAccess !==
|
|
841
|
+
false
|
|
842
|
+
}
|
|
843
|
+
onChange={e =>
|
|
844
|
+
updateCustomSetting(
|
|
845
|
+
'webSearch',
|
|
846
|
+
'externalWebAccess',
|
|
847
|
+
e.target.checked
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
/>
|
|
851
|
+
}
|
|
852
|
+
label={trans.__('Use External Web Access')}
|
|
853
|
+
/>
|
|
854
|
+
</>
|
|
855
|
+
)}
|
|
856
|
+
|
|
857
|
+
{webSearchImplementation === 'anthropic' && (
|
|
858
|
+
<>
|
|
859
|
+
<TextField
|
|
860
|
+
fullWidth
|
|
861
|
+
label={trans.__('Web Search Max Uses')}
|
|
862
|
+
type="number"
|
|
863
|
+
value={webSearchSettings.maxUses ?? ''}
|
|
864
|
+
onChange={e =>
|
|
865
|
+
updateCustomSetting(
|
|
866
|
+
'webSearch',
|
|
867
|
+
'maxUses',
|
|
868
|
+
e.target.value
|
|
869
|
+
? Number(e.target.value)
|
|
870
|
+
: undefined
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
slotProps={{ htmlInput: { min: 1 } }}
|
|
874
|
+
/>
|
|
875
|
+
{renderDomainList(
|
|
876
|
+
'webSearch.blockedDomains',
|
|
877
|
+
trans.__('Blocked Domains'),
|
|
878
|
+
trans.__('spam.example.com'),
|
|
879
|
+
webSearchSettings.blockedDomains
|
|
880
|
+
)}
|
|
881
|
+
</>
|
|
882
|
+
)}
|
|
883
|
+
</Box>
|
|
884
|
+
)}
|
|
885
|
+
</>
|
|
886
|
+
)}
|
|
887
|
+
|
|
888
|
+
{supportsWebFetch && (
|
|
889
|
+
<>
|
|
890
|
+
<FormControlLabel
|
|
891
|
+
control={
|
|
892
|
+
<Switch
|
|
893
|
+
checked={webFetchSettings.enabled === true}
|
|
894
|
+
onChange={e =>
|
|
895
|
+
updateCustomSetting(
|
|
896
|
+
'webFetch',
|
|
897
|
+
'enabled',
|
|
898
|
+
e.target.checked
|
|
899
|
+
)
|
|
900
|
+
}
|
|
901
|
+
/>
|
|
902
|
+
}
|
|
903
|
+
label={trans.__('Enable Web Fetch')}
|
|
904
|
+
/>
|
|
905
|
+
|
|
906
|
+
{webFetchSettings.enabled === true && (
|
|
907
|
+
<Box
|
|
908
|
+
sx={{
|
|
909
|
+
pl: 2,
|
|
910
|
+
borderLeft: 2,
|
|
911
|
+
borderColor: 'divider',
|
|
912
|
+
display: 'flex',
|
|
913
|
+
flexDirection: 'column',
|
|
914
|
+
gap: 1.5
|
|
915
|
+
}}
|
|
916
|
+
>
|
|
917
|
+
<TextField
|
|
918
|
+
fullWidth
|
|
919
|
+
label={trans.__('Web Fetch Max Uses')}
|
|
920
|
+
type="number"
|
|
921
|
+
value={webFetchSettings.maxUses ?? ''}
|
|
922
|
+
onChange={e =>
|
|
923
|
+
updateCustomSetting(
|
|
924
|
+
'webFetch',
|
|
925
|
+
'maxUses',
|
|
926
|
+
e.target.value
|
|
927
|
+
? Number(e.target.value)
|
|
928
|
+
: undefined
|
|
929
|
+
)
|
|
930
|
+
}
|
|
931
|
+
slotProps={{ htmlInput: { min: 1 } }}
|
|
932
|
+
/>
|
|
933
|
+
<TextField
|
|
934
|
+
fullWidth
|
|
935
|
+
label={trans.__('Web Fetch Max Content Tokens')}
|
|
936
|
+
type="number"
|
|
937
|
+
value={webFetchSettings.maxContentTokens ?? ''}
|
|
938
|
+
onChange={e =>
|
|
939
|
+
updateCustomSetting(
|
|
940
|
+
'webFetch',
|
|
941
|
+
'maxContentTokens',
|
|
942
|
+
e.target.value
|
|
943
|
+
? Number(e.target.value)
|
|
944
|
+
: undefined
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
slotProps={{ htmlInput: { min: 1 } }}
|
|
948
|
+
/>
|
|
949
|
+
{renderDomainList(
|
|
950
|
+
'webFetch.allowedDomains',
|
|
951
|
+
trans.__('Allowed Domains'),
|
|
952
|
+
trans.__('docs.example.com'),
|
|
953
|
+
webFetchSettings.allowedDomains
|
|
954
|
+
)}
|
|
955
|
+
{renderDomainList(
|
|
956
|
+
'webFetch.blockedDomains',
|
|
957
|
+
trans.__('Blocked Domains'),
|
|
958
|
+
trans.__('spam.example.com'),
|
|
959
|
+
webFetchSettings.blockedDomains
|
|
960
|
+
)}
|
|
961
|
+
<FormControlLabel
|
|
962
|
+
control={
|
|
963
|
+
<Switch
|
|
964
|
+
checked={
|
|
965
|
+
webFetchSettings.citationsEnabled === true
|
|
966
|
+
}
|
|
967
|
+
onChange={e =>
|
|
968
|
+
updateCustomSetting(
|
|
969
|
+
'webFetch',
|
|
970
|
+
'citationsEnabled',
|
|
971
|
+
e.target.checked
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
/>
|
|
975
|
+
}
|
|
976
|
+
label={trans.__('Enable Citations')}
|
|
977
|
+
/>
|
|
978
|
+
</Box>
|
|
979
|
+
)}
|
|
980
|
+
</>
|
|
981
|
+
)}
|
|
982
|
+
</>
|
|
983
|
+
)}
|
|
984
|
+
</Box>
|
|
985
|
+
</AccordionDetails>
|
|
986
|
+
</Accordion>
|
|
987
|
+
</Box>
|
|
988
|
+
</DialogContent>
|
|
989
|
+
<DialogActions>
|
|
990
|
+
<Button onClick={onClose}>{trans.__('Cancel')}</Button>
|
|
991
|
+
<Button onClick={handleSave} variant="contained" disabled={!isValid}>
|
|
992
|
+
{mode === 'add' ? trans.__('Add Provider') : trans.__('Save Changes')}
|
|
993
|
+
</Button>
|
|
994
|
+
</DialogActions>
|
|
995
|
+
</Dialog>
|
|
996
|
+
);
|
|
997
|
+
};
|