@jupyterlite/ai 0.11.1 → 0.13.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/agent.d.ts +61 -7
- package/lib/agent.js +286 -103
- package/lib/chat-commands/clear.d.ts +8 -0
- package/lib/chat-commands/clear.js +30 -0
- package/lib/chat-commands/index.d.ts +2 -0
- package/lib/chat-commands/index.js +2 -0
- package/lib/chat-commands/skills.d.ts +19 -0
- package/lib/chat-commands/skills.js +57 -0
- package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
- package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
- package/lib/chat-model.d.ts +16 -0
- package/lib/chat-model.js +191 -11
- package/lib/completion/completion-provider.d.ts +1 -1
- package/lib/completion/completion-provider.js +14 -2
- package/lib/components/model-select.js +4 -4
- package/lib/components/tool-select.d.ts +11 -2
- package/lib/components/tool-select.js +77 -18
- package/lib/index.d.ts +3 -3
- package/lib/index.js +311 -72
- package/lib/models/settings-model.d.ts +3 -0
- package/lib/models/settings-model.js +63 -14
- package/lib/providers/built-in-providers.js +12 -7
- package/lib/providers/provider-tools.d.ts +36 -0
- package/lib/providers/provider-tools.js +93 -0
- package/lib/rendered-message-outputarea.d.ts +24 -0
- package/lib/rendered-message-outputarea.js +48 -0
- package/lib/skills/index.d.ts +4 -0
- package/lib/skills/index.js +7 -0
- package/lib/skills/parse-skill.d.ts +25 -0
- package/lib/skills/parse-skill.js +69 -0
- package/lib/skills/skill-loader.d.ts +25 -0
- package/lib/skills/skill-loader.js +133 -0
- package/lib/skills/skill-registry.d.ts +31 -0
- package/lib/skills/skill-registry.js +100 -0
- package/lib/skills/types.d.ts +29 -0
- package/lib/skills/types.js +5 -0
- package/lib/tokens.d.ts +77 -7
- package/lib/tokens.js +6 -1
- package/lib/tools/commands.js +4 -2
- package/lib/tools/skills.d.ts +9 -0
- package/lib/tools/skills.js +73 -0
- package/lib/tools/web.d.ts +8 -0
- package/lib/tools/web.js +196 -0
- package/lib/widgets/ai-settings.d.ts +1 -1
- package/lib/widgets/ai-settings.js +157 -38
- package/lib/widgets/main-area-chat.d.ts +6 -0
- package/lib/widgets/main-area-chat.js +28 -0
- package/lib/widgets/provider-config-dialog.js +207 -4
- package/package.json +18 -11
- package/schema/settings-model.json +97 -2
- package/src/agent.ts +397 -123
- package/src/chat-commands/clear.ts +46 -0
- package/src/chat-commands/index.ts +2 -0
- package/src/chat-commands/skills.ts +87 -0
- package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
- package/src/chat-model.ts +270 -23
- package/src/completion/completion-provider.ts +26 -12
- package/src/components/model-select.tsx +4 -5
- package/src/components/tool-select.tsx +110 -7
- package/src/index.ts +395 -87
- package/src/models/settings-model.ts +70 -15
- package/src/providers/built-in-providers.ts +12 -7
- package/src/providers/provider-tools.ts +179 -0
- package/src/rendered-message-outputarea.ts +62 -0
- package/src/skills/index.ts +14 -0
- package/src/skills/parse-skill.ts +91 -0
- package/src/skills/skill-loader.ts +175 -0
- package/src/skills/skill-registry.ts +137 -0
- package/src/skills/types.ts +37 -0
- package/src/tokens.ts +109 -9
- package/src/tools/commands.ts +4 -2
- package/src/tools/skills.ts +84 -0
- package/src/tools/web.ts +238 -0
- package/src/widgets/ai-settings.tsx +357 -77
- package/src/widgets/main-area-chat.ts +34 -1
- package/src/widgets/provider-config-dialog.tsx +496 -3
|
@@ -8,9 +8,10 @@ import Delete from '@mui/icons-material/Delete';
|
|
|
8
8
|
import Edit from '@mui/icons-material/Edit';
|
|
9
9
|
import Error from '@mui/icons-material/Error';
|
|
10
10
|
import ErrorOutline from '@mui/icons-material/ErrorOutline';
|
|
11
|
+
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
|
11
12
|
import MoreVert from '@mui/icons-material/MoreVert';
|
|
12
13
|
import Settings from '@mui/icons-material/Settings';
|
|
13
|
-
import { Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, IconButton, InputLabel, List, ListItem,
|
|
14
|
+
import { Alert, Box, Button, Card, CardContent, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControl, FormControlLabel, IconButton, InputLabel, List, ListItem, ListItemText, Menu, MenuItem, Select, Switch, Tab, Tabs, TextField, ThemeProvider, Tooltip, Typography, createTheme } from '@mui/material';
|
|
14
15
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
15
16
|
import { SECRETS_NAMESPACE, SECRETS_REPLACEMENT } from '../tokens';
|
|
16
17
|
import { ProviderConfigDialog } from './provider-config-dialog';
|
|
@@ -51,6 +52,11 @@ export class AISettingsWidget extends ReactWidget {
|
|
|
51
52
|
this.title.label = this._trans.__('AI Settings');
|
|
52
53
|
this.title.caption = this._trans.__('Configure AI providers and behavior');
|
|
53
54
|
this.title.closable = true;
|
|
55
|
+
// Disable the secrets manager if the token is empty.
|
|
56
|
+
if (!options.token) {
|
|
57
|
+
this._settingsModel.updateConfig({ useSecretsManager: false });
|
|
58
|
+
this._secretsManager = undefined;
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
62
|
* Render the AI settings component
|
|
@@ -168,11 +174,19 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
168
174
|
void promptDebouncer.invoke();
|
|
169
175
|
};
|
|
170
176
|
const getSecretFromManager = async (provider, fieldName) => {
|
|
171
|
-
const
|
|
177
|
+
const token = Private.getToken();
|
|
178
|
+
if (!token) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const secret = await secretsManager?.get(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`);
|
|
172
182
|
return secret?.value;
|
|
173
183
|
};
|
|
174
184
|
const setSecretToManager = async (provider, fieldName, value) => {
|
|
175
|
-
|
|
185
|
+
const token = Private.getToken();
|
|
186
|
+
if (!token) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await secretsManager?.set(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`, {
|
|
176
190
|
namespace: SECRETS_NAMESPACE,
|
|
177
191
|
id: `${provider}:${fieldName}`,
|
|
178
192
|
value
|
|
@@ -188,7 +202,11 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
188
202
|
if (!(model.config.useSecretsManager && secretsManager)) {
|
|
189
203
|
return;
|
|
190
204
|
}
|
|
191
|
-
|
|
205
|
+
const token = Private.getToken();
|
|
206
|
+
if (!token) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await secretsManager?.attach(token, SECRETS_NAMESPACE, `${provider}:${fieldName}`, input);
|
|
192
210
|
};
|
|
193
211
|
/**
|
|
194
212
|
* Handle adding a new AI provider
|
|
@@ -270,22 +288,29 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
270
288
|
if (updates.useSecretsManager !== undefined) {
|
|
271
289
|
if (updates.useSecretsManager) {
|
|
272
290
|
for (const provider of model.config.providers) {
|
|
273
|
-
|
|
291
|
+
const settingsApiKey = provider.apiKey;
|
|
292
|
+
// If the secrets manager doesn't have the current API key, set the current
|
|
274
293
|
// one from settings.
|
|
294
|
+
// Update the settings value with SECRETS_REPLACEMENT if a key exist in the
|
|
295
|
+
// secrets manager (was already there or a value was set in settings).
|
|
275
296
|
if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
|
|
276
|
-
|
|
297
|
+
if (settingsApiKey !== undefined) {
|
|
298
|
+
setSecretToManager(provider.provider, 'apiKey', settingsApiKey !== SECRETS_REPLACEMENT ? settingsApiKey : '');
|
|
299
|
+
provider.apiKey = SECRETS_REPLACEMENT;
|
|
300
|
+
await model.updateProvider(provider.id, provider);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
provider.apiKey = SECRETS_REPLACEMENT;
|
|
305
|
+
await model.updateProvider(provider.id, provider);
|
|
277
306
|
}
|
|
278
|
-
provider.apiKey = SECRETS_REPLACEMENT;
|
|
279
|
-
await model.updateProvider(provider.id, provider);
|
|
280
307
|
}
|
|
281
308
|
}
|
|
282
309
|
else {
|
|
283
310
|
for (const provider of model.config.providers) {
|
|
284
311
|
const apiKey = await getSecretFromManager(provider.provider, 'apiKey');
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
await model.updateProvider(provider.id, provider);
|
|
288
|
-
}
|
|
312
|
+
provider.apiKey = apiKey;
|
|
313
|
+
await model.updateProvider(provider.id, provider);
|
|
289
314
|
}
|
|
290
315
|
}
|
|
291
316
|
}
|
|
@@ -399,7 +424,13 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
399
424
|
const isActiveCompleter = config.useSameProviderForChatAndCompleter
|
|
400
425
|
? isActive
|
|
401
426
|
: config.activeCompleterProvider === provider.id;
|
|
427
|
+
const providerInfo = providerRegistry.getProviderInfo(provider.provider);
|
|
428
|
+
const providerToolCapabilities = providerInfo?.providerToolCapabilities;
|
|
402
429
|
const params = provider.parameters;
|
|
430
|
+
const webSearchEnabled = !!providerToolCapabilities?.webSearch &&
|
|
431
|
+
provider.customSettings?.webSearch?.enabled === true;
|
|
432
|
+
const webFetchEnabled = !!providerToolCapabilities?.webFetch &&
|
|
433
|
+
provider.customSettings?.webFetch?.enabled === true;
|
|
403
434
|
return (React.createElement(ListItem, { key: provider.id, sx: {
|
|
404
435
|
flexDirection: 'column',
|
|
405
436
|
alignItems: 'stretch',
|
|
@@ -428,18 +459,21 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
428
459
|
provider.model,
|
|
429
460
|
provider.description &&
|
|
430
461
|
` • ${provider.description}`),
|
|
431
|
-
params
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
462
|
+
(params?.temperature !== undefined ||
|
|
463
|
+
params?.maxOutputTokens !== undefined ||
|
|
464
|
+
params?.maxTurns !== undefined ||
|
|
465
|
+
webSearchEnabled ||
|
|
466
|
+
webFetchEnabled) && (React.createElement(Box, { sx: {
|
|
435
467
|
display: 'flex',
|
|
436
468
|
flexWrap: 'wrap',
|
|
437
469
|
gap: 1,
|
|
438
470
|
mt: 1
|
|
439
471
|
} },
|
|
440
|
-
params
|
|
441
|
-
params
|
|
442
|
-
params
|
|
472
|
+
params?.temperature !== undefined && (React.createElement(Chip, { label: trans.__('Temp: %1', params.temperature), size: "small", variant: "outlined" })),
|
|
473
|
+
params?.maxOutputTokens !== undefined && (React.createElement(Chip, { label: trans.__('Tokens: %1', params.maxOutputTokens), size: "small", variant: "outlined" })),
|
|
474
|
+
params?.maxTurns !== undefined && (React.createElement(Chip, { label: trans.__('Turns: %1', params.maxTurns), size: "small", variant: "outlined" })),
|
|
475
|
+
webSearchEnabled && (React.createElement(Chip, { label: trans.__('Web Search'), size: "small", variant: "outlined", color: "info" })),
|
|
476
|
+
webFetchEnabled && (React.createElement(Chip, { label: trans.__('Web Fetch'), size: "small", variant: "outlined", color: "info" }))))),
|
|
443
477
|
React.createElement(IconButton, { onClick: e => handleMenuClick(e, provider.id), size: "small" },
|
|
444
478
|
React.createElement(MoreVert, null)))));
|
|
445
479
|
}))))),
|
|
@@ -488,22 +522,51 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
488
522
|
React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: trans.__('System Prompt'), value: systemPromptValue, onChange: e => handleSystemPromptChange(e.target.value), placeholder: trans.__("Define the AI's behavior and personality..."), helperText: trans.__('Instructions that define how the AI should behave and respond') }),
|
|
489
523
|
React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: trans.__('Completion System Prompt'), value: completionPromptValue, onChange: e => handleCompletionPromptChange(e.target.value), placeholder: trans.__('Define how the AI should generate code completions...'), helperText: trans.__('Instructions that define how the AI should generate code completions') }),
|
|
490
524
|
React.createElement(Divider, { sx: { my: 2 } }),
|
|
525
|
+
React.createElement(Box, null,
|
|
526
|
+
React.createElement(Typography, { variant: "body1", gutterBottom: true, sx: {
|
|
527
|
+
display: 'inline-flex',
|
|
528
|
+
alignItems: 'center',
|
|
529
|
+
gap: 1
|
|
530
|
+
} },
|
|
531
|
+
trans.__('Skills Paths'),
|
|
532
|
+
React.createElement(Tooltip, { title: trans.__('Directories containing agent skills, relative to the server root. Skills are loaded from all paths; the first occurrence of a skill name takes priority.') },
|
|
533
|
+
React.createElement(InfoOutlined, { sx: { fontSize: 16 } }))),
|
|
534
|
+
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.skillsPaths ?? []).map((skillPath, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
|
|
535
|
+
const newPaths = [...config.skillsPaths];
|
|
536
|
+
newPaths.splice(index, 1);
|
|
537
|
+
handleConfigUpdate({ skillsPaths: newPaths });
|
|
538
|
+
}, size: "small" },
|
|
539
|
+
React.createElement(Delete, null)) },
|
|
540
|
+
React.createElement(ListItemText, { primary: skillPath }))))),
|
|
541
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Add Skills Path'), placeholder: trans.__('e.g., .claude/skills'), onKeyDown: e => {
|
|
542
|
+
if (e.key === 'Enter') {
|
|
543
|
+
const value = e.target.value.trim();
|
|
544
|
+
if (value &&
|
|
545
|
+
!(config.skillsPaths ?? []).includes(value)) {
|
|
546
|
+
const newPaths = [
|
|
547
|
+
...(config.skillsPaths ?? []),
|
|
548
|
+
value
|
|
549
|
+
];
|
|
550
|
+
handleConfigUpdate({ skillsPaths: newPaths });
|
|
551
|
+
e.target.value = '';
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}, helperText: trans.__('Press Enter to add a path. Defaults: .agents/skills, _agents/skills') })),
|
|
555
|
+
React.createElement(Divider, { sx: { my: 2 } }),
|
|
491
556
|
React.createElement(Box, null,
|
|
492
557
|
React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Requiring Approval')),
|
|
493
558
|
React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Commands that require user approval before AI can execute them')),
|
|
494
|
-
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, config.commandsRequiringApproval.map((command, index) => (React.createElement(ListItem, { key: index, divider: true
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}, size: "small" },
|
|
506
|
-
React.createElement(Delete, null))))))),
|
|
559
|
+
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, config.commandsRequiringApproval.map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
|
|
560
|
+
const newCommands = [
|
|
561
|
+
...config.commandsRequiringApproval
|
|
562
|
+
];
|
|
563
|
+
newCommands.splice(index, 1);
|
|
564
|
+
handleConfigUpdate({
|
|
565
|
+
commandsRequiringApproval: newCommands
|
|
566
|
+
});
|
|
567
|
+
}, size: "small" },
|
|
568
|
+
React.createElement(Delete, null)) },
|
|
569
|
+
React.createElement(ListItemText, { primary: command }))))),
|
|
507
570
|
React.createElement(TextField, { fullWidth: true, label: trans.__('Add New Command'), placeholder: trans.__('e.g., notebook:run-cell'), onKeyDown: e => {
|
|
508
571
|
if (e.key === 'Enter') {
|
|
509
572
|
const value = e.target.value.trim();
|
|
@@ -519,7 +582,65 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
519
582
|
e.target.value = '';
|
|
520
583
|
}
|
|
521
584
|
}
|
|
522
|
-
}, helperText: trans.__('Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code') }))
|
|
585
|
+
}, helperText: trans.__('Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code') })),
|
|
586
|
+
React.createElement(Divider, { sx: { my: 2 } }),
|
|
587
|
+
React.createElement(Box, null,
|
|
588
|
+
React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Auto-Rendering MIME Bundles')),
|
|
589
|
+
React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Only these execute_command command IDs can auto-render MIME bundle outputs in chat')),
|
|
590
|
+
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.commandsAutoRenderMimeBundles ?? []).map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
|
|
591
|
+
const newCommands = [
|
|
592
|
+
...(config.commandsAutoRenderMimeBundles ??
|
|
593
|
+
[])
|
|
594
|
+
];
|
|
595
|
+
newCommands.splice(index, 1);
|
|
596
|
+
handleConfigUpdate({
|
|
597
|
+
commandsAutoRenderMimeBundles: newCommands
|
|
598
|
+
});
|
|
599
|
+
}, size: "small" },
|
|
600
|
+
React.createElement(Delete, null)) },
|
|
601
|
+
React.createElement(ListItemText, { primary: command }))))),
|
|
602
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Add Auto-Render Command'), placeholder: trans.__('e.g., jupyterlab-ai-commands:execute-in-kernel'), onKeyDown: e => {
|
|
603
|
+
if (e.key === 'Enter') {
|
|
604
|
+
const value = e.target.value.trim();
|
|
605
|
+
const existingCommands = config.commandsAutoRenderMimeBundles ?? [];
|
|
606
|
+
if (value && !existingCommands.includes(value)) {
|
|
607
|
+
const newCommands = [...existingCommands, value];
|
|
608
|
+
handleConfigUpdate({
|
|
609
|
+
commandsAutoRenderMimeBundles: newCommands
|
|
610
|
+
});
|
|
611
|
+
e.target.value = '';
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}, helperText: trans.__('Press Enter to add a command. Default: jupyterlab-ai-commands:execute-in-kernel') })),
|
|
615
|
+
React.createElement(Divider, { sx: { my: 2 } }),
|
|
616
|
+
React.createElement(Box, null,
|
|
617
|
+
React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Trusted MIME Types for Auto-Render')),
|
|
618
|
+
React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('When auto-rendering command outputs, these MIME types are marked trusted in chat')),
|
|
619
|
+
React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.trustedMimeTypesForAutoRender ?? []).map((mimeType, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
|
|
620
|
+
const newMimeTypes = [
|
|
621
|
+
...(config.trustedMimeTypesForAutoRender ??
|
|
622
|
+
[])
|
|
623
|
+
];
|
|
624
|
+
newMimeTypes.splice(index, 1);
|
|
625
|
+
handleConfigUpdate({
|
|
626
|
+
trustedMimeTypesForAutoRender: newMimeTypes
|
|
627
|
+
});
|
|
628
|
+
}, size: "small" },
|
|
629
|
+
React.createElement(Delete, null)) },
|
|
630
|
+
React.createElement(ListItemText, { primary: mimeType }))))),
|
|
631
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Add Trusted MIME Type'), placeholder: trans.__('e.g., text/html'), onKeyDown: e => {
|
|
632
|
+
if (e.key === 'Enter') {
|
|
633
|
+
const value = e.target.value.trim();
|
|
634
|
+
const existingMimeTypes = config.trustedMimeTypesForAutoRender ?? [];
|
|
635
|
+
if (value && !existingMimeTypes.includes(value)) {
|
|
636
|
+
const newMimeTypes = [...existingMimeTypes, value];
|
|
637
|
+
handleConfigUpdate({
|
|
638
|
+
trustedMimeTypesForAutoRender: newMimeTypes
|
|
639
|
+
});
|
|
640
|
+
e.target.value = '';
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}, helperText: trans.__('Press Enter to add a MIME type. Default: text/html') })))))),
|
|
523
644
|
activeTab === 2 && (React.createElement(Card, { elevation: 2 },
|
|
524
645
|
React.createElement(CardContent, null,
|
|
525
646
|
React.createElement(Box, { sx: {
|
|
@@ -533,7 +654,8 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
533
654
|
React.createElement(Typography, { variant: "h6", component: "h2" }, trans.__('Remote MCP Servers'))),
|
|
534
655
|
React.createElement(Button, { variant: "contained", startIcon: React.createElement(Add, null), onClick: openAddMCPDialog, size: "small" }, trans.__('Add Server'))),
|
|
535
656
|
React.createElement(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 } }, trans.__("Configure remote Model Context Protocol (MCP) servers to extend the AI's capabilities with external tools and data sources.")),
|
|
536
|
-
config.mcpServers.length === 0 ? (React.createElement(Alert, { severity: "info" }, trans.__('No MCP servers configured yet. Click "Add Server" to connect to remote MCP services.'))) : (React.createElement(List, null, config.mcpServers.map(server => (React.createElement(ListItem, { key: server.id, divider: true },
|
|
657
|
+
config.mcpServers.length === 0 ? (React.createElement(Alert, { severity: "info" }, trans.__('No MCP servers configured yet. Click "Add Server" to connect to remote MCP services.'))) : (React.createElement(List, null, config.mcpServers.map(server => (React.createElement(ListItem, { key: server.id, divider: true, secondaryAction: React.createElement(IconButton, { onClick: e => handleMCPMenuClick(e, server.id), size: "small" },
|
|
658
|
+
React.createElement(MoreVert, null)) },
|
|
537
659
|
React.createElement(ListItemText, { primary: React.createElement(Box, { sx: {
|
|
538
660
|
display: 'flex',
|
|
539
661
|
alignItems: 'center',
|
|
@@ -550,10 +672,7 @@ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, provide
|
|
|
550
672
|
React.createElement(Typography, { variant: "body2", color: "text.secondary" }, server.url),
|
|
551
673
|
server.enabled && agentManagerFactory && (React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Status: %1', agentManagerFactory.isMCPServerConnected(server.name)
|
|
552
674
|
? trans.__('Connected')
|
|
553
|
-
: trans.__('Connection failed'))))) }),
|
|
554
|
-
React.createElement(ListItemSecondaryAction, null,
|
|
555
|
-
React.createElement(IconButton, { onClick: e => handleMCPMenuClick(e, server.id), size: "small" },
|
|
556
|
-
React.createElement(MoreVert, null))))))))))),
|
|
675
|
+
: trans.__('Connection failed'))))) }))))))))),
|
|
557
676
|
React.createElement(ProviderConfigDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSave: editingProvider ? handleEditProvider : handleAddProvider, initialConfig: editingProvider, mode: editingProvider ? 'edit' : 'add', providerRegistry: providerRegistry, handleSecretField: handleSecretField, trans: trans }),
|
|
558
677
|
React.createElement(Menu, { anchorEl: menuAnchor, open: Boolean(menuAnchor), onClose: handleMenuClose },
|
|
559
678
|
React.createElement(MenuItem, { onClick: () => {
|
|
@@ -21,5 +21,11 @@ export declare class MainAreaChat extends MainAreaWidget<ChatWidget> {
|
|
|
21
21
|
* Get the model of the chat.
|
|
22
22
|
*/
|
|
23
23
|
get model(): AIChatModel;
|
|
24
|
+
/**
|
|
25
|
+
* Get the area of the chat.
|
|
26
|
+
*/
|
|
27
|
+
get area(): string | undefined;
|
|
28
|
+
private _writersChanged;
|
|
24
29
|
private _approvalButtons;
|
|
30
|
+
private _outputAreaCompat;
|
|
25
31
|
}
|
|
@@ -2,6 +2,7 @@ import { CommandToolbarButton, MainAreaWidget } from '@jupyterlab/apputils';
|
|
|
2
2
|
import { launchIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { ApprovalButtons } from '../approval-buttons';
|
|
4
4
|
import { TokenUsageWidget } from '../components/token-usage-display';
|
|
5
|
+
import { RenderedMessageOutputAreaCompat } from '../rendered-message-outputarea';
|
|
5
6
|
import { CommandIds } from '../tokens';
|
|
6
7
|
/**
|
|
7
8
|
* The chat as a main area widget.
|
|
@@ -34,11 +35,19 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
34
35
|
chatPanel: this.content,
|
|
35
36
|
agentManager: this.model.agentManager
|
|
36
37
|
});
|
|
38
|
+
// Temporary compat: keep output-area CSS context for MIME renderers
|
|
39
|
+
// until jupyter-chat provides it natively.
|
|
40
|
+
this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
|
|
41
|
+
chatPanel: this.content
|
|
42
|
+
});
|
|
43
|
+
this.model.writersChanged.connect(this._writersChanged);
|
|
37
44
|
}
|
|
38
45
|
dispose() {
|
|
39
46
|
super.dispose();
|
|
40
47
|
// Dispose of the approval buttons widget when the chat is disposed.
|
|
41
48
|
this._approvalButtons.dispose();
|
|
49
|
+
this._outputAreaCompat.dispose();
|
|
50
|
+
this.model.writersChanged.disconnect(this._writersChanged);
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Get the model of the chat.
|
|
@@ -46,5 +55,24 @@ export class MainAreaChat extends MainAreaWidget {
|
|
|
46
55
|
get model() {
|
|
47
56
|
return this.content.model;
|
|
48
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the area of the chat.
|
|
60
|
+
*/
|
|
61
|
+
get area() {
|
|
62
|
+
return this.content.area;
|
|
63
|
+
}
|
|
64
|
+
_writersChanged = (_, writers) => {
|
|
65
|
+
// Check if AI is currently writing (streaming)
|
|
66
|
+
const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
|
|
67
|
+
if (aiWriting) {
|
|
68
|
+
this.content.inputToolbarRegistry?.hide('send');
|
|
69
|
+
this.content.inputToolbarRegistry?.show('stop');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.content.inputToolbarRegistry?.hide('stop');
|
|
73
|
+
this.content.inputToolbarRegistry?.show('send');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
49
76
|
_approvalButtons;
|
|
77
|
+
_outputAreaCompat;
|
|
50
78
|
}
|
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
|
2
|
+
import Delete from '@mui/icons-material/Delete';
|
|
2
3
|
import Visibility from '@mui/icons-material/Visibility';
|
|
3
4
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|
4
|
-
import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
|
|
5
|
+
import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, IconButton, InputAdornment, InputLabel, List, ListItem, ListItemText, MenuItem, Select, Slider, Switch, TextField, Typography } from '@mui/material';
|
|
5
6
|
import React from 'react';
|
|
6
7
|
/**
|
|
7
8
|
* Default parameter values for provider configuration
|
|
8
9
|
*/
|
|
9
10
|
const DEFAULT_TEMPERATURE = 0.7;
|
|
10
11
|
const DEFAULT_MAX_TURNS = 25;
|
|
12
|
+
const DOMAIN_FIELD_MAP = {
|
|
13
|
+
'webSearch.allowedDomains': {
|
|
14
|
+
section: 'webSearch',
|
|
15
|
+
key: 'allowedDomains'
|
|
16
|
+
},
|
|
17
|
+
'webSearch.blockedDomains': {
|
|
18
|
+
section: 'webSearch',
|
|
19
|
+
key: 'blockedDomains'
|
|
20
|
+
},
|
|
21
|
+
'webFetch.allowedDomains': {
|
|
22
|
+
section: 'webFetch',
|
|
23
|
+
key: 'allowedDomains'
|
|
24
|
+
},
|
|
25
|
+
'webFetch.blockedDomains': {
|
|
26
|
+
section: 'webFetch',
|
|
27
|
+
key: 'blockedDomains'
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function createEmptyDomainInputs() {
|
|
31
|
+
return {
|
|
32
|
+
'webSearch.allowedDomains': '',
|
|
33
|
+
'webSearch.blockedDomains': '',
|
|
34
|
+
'webFetch.allowedDomains': '',
|
|
35
|
+
'webFetch.blockedDomains': ''
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function toRecord(value) {
|
|
39
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
function toStringArray(value) {
|
|
45
|
+
if (!Array.isArray(value)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return value.filter((item) => typeof item === 'string');
|
|
49
|
+
}
|
|
50
|
+
function sanitizeCustomSettingsForProvider(customSettings, capabilities) {
|
|
51
|
+
const result = { ...customSettings };
|
|
52
|
+
const webSearch = toRecord(customSettings.webSearch);
|
|
53
|
+
const webFetch = toRecord(customSettings.webFetch);
|
|
54
|
+
const supportsWebSearch = !!capabilities?.webSearch;
|
|
55
|
+
const supportsWebFetch = !!capabilities?.webFetch;
|
|
56
|
+
if (supportsWebSearch && webSearch.enabled === true) {
|
|
57
|
+
result.webSearch = webSearch;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
delete result.webSearch;
|
|
61
|
+
}
|
|
62
|
+
if (supportsWebFetch && webFetch.enabled === true) {
|
|
63
|
+
result.webFetch = webFetch;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
delete result.webFetch;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
11
70
|
export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mode, providerRegistry, handleSecretField, trans }) => {
|
|
12
71
|
const apiKeyRef = React.useRef();
|
|
13
72
|
const [name, setName] = React.useState(initialConfig?.name || '');
|
|
@@ -16,8 +75,17 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
16
75
|
const [apiKey, setApiKey] = React.useState(initialConfig?.apiKey || '');
|
|
17
76
|
const [baseURL, setBaseURL] = React.useState(initialConfig?.baseURL || '');
|
|
18
77
|
const [showApiKey, setShowApiKey] = React.useState(false);
|
|
78
|
+
const [customSettings, setCustomSettings] = React.useState(initialConfig?.customSettings || {});
|
|
79
|
+
const [domainInputs, setDomainInputs] = React.useState(createEmptyDomainInputs());
|
|
19
80
|
const [parameters, setParameters] = React.useState(initialConfig?.parameters || {});
|
|
20
81
|
const [expandedAdvanced, setExpandedAdvanced] = React.useState(false);
|
|
82
|
+
const selectedProviderInfo = React.useMemo(() => providerRegistry.getProviderInfo(provider), [providerRegistry, provider]);
|
|
83
|
+
const providerToolCapabilities = selectedProviderInfo?.providerToolCapabilities;
|
|
84
|
+
const webSearchImplementation = providerToolCapabilities?.webSearch?.implementation;
|
|
85
|
+
const supportsWebSearch = !!providerToolCapabilities?.webSearch;
|
|
86
|
+
const supportsWebFetch = !!providerToolCapabilities?.webFetch;
|
|
87
|
+
const webSearchSettings = React.useMemo(() => toRecord(customSettings.webSearch), [customSettings]);
|
|
88
|
+
const webFetchSettings = React.useMemo(() => toRecord(customSettings.webFetch), [customSettings]);
|
|
21
89
|
// Get provider options from registry
|
|
22
90
|
const providerOptions = React.useMemo(() => {
|
|
23
91
|
const providers = providerRegistry.providers;
|
|
@@ -45,11 +113,14 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
45
113
|
setApiKey(initialConfig?.apiKey || '');
|
|
46
114
|
setBaseURL(initialConfig?.baseURL || '');
|
|
47
115
|
setParameters(initialConfig?.parameters || {});
|
|
116
|
+
setCustomSettings(initialConfig?.customSettings || {});
|
|
117
|
+
setDomainInputs(createEmptyDomainInputs());
|
|
48
118
|
setShowApiKey(false);
|
|
49
119
|
setExpandedAdvanced(false);
|
|
50
120
|
}
|
|
51
121
|
else {
|
|
52
122
|
// Reset expanded state when dialog closes
|
|
123
|
+
setDomainInputs(createEmptyDomainInputs());
|
|
53
124
|
setExpandedAdvanced(false);
|
|
54
125
|
}
|
|
55
126
|
}, [open, initialConfig]);
|
|
@@ -65,20 +136,103 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
65
136
|
if (open && apiKeyRef.current) {
|
|
66
137
|
handleSecretField(apiKeyRef.current, provider, 'apiKey');
|
|
67
138
|
}
|
|
68
|
-
}, [open, provider,
|
|
139
|
+
}, [open, provider, handleSecretField]);
|
|
140
|
+
const updateCustomSetting = React.useCallback((section, key, value) => {
|
|
141
|
+
setCustomSettings(prev => {
|
|
142
|
+
const next = { ...prev };
|
|
143
|
+
const sectionSettings = { ...toRecord(next[section]) };
|
|
144
|
+
const shouldDelete = value === undefined ||
|
|
145
|
+
value === null ||
|
|
146
|
+
value === '' ||
|
|
147
|
+
(Array.isArray(value) && value.length === 0);
|
|
148
|
+
if (shouldDelete) {
|
|
149
|
+
delete sectionSettings[key];
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
sectionSettings[key] = value;
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(sectionSettings).length === 0) {
|
|
155
|
+
delete next[section];
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
next[section] = sectionSettings;
|
|
159
|
+
}
|
|
160
|
+
return next;
|
|
161
|
+
});
|
|
162
|
+
}, []);
|
|
163
|
+
const addDomainValue = React.useCallback((fieldId) => {
|
|
164
|
+
const valueToAdd = domainInputs[fieldId].trim();
|
|
165
|
+
if (!valueToAdd) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const { section, key } = DOMAIN_FIELD_MAP[fieldId];
|
|
169
|
+
const currentValues = toStringArray(toRecord(customSettings[section])[key]);
|
|
170
|
+
if (currentValues.includes(valueToAdd)) {
|
|
171
|
+
setDomainInputs(prev => ({
|
|
172
|
+
...prev,
|
|
173
|
+
[fieldId]: ''
|
|
174
|
+
}));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const nextValues = [...currentValues, valueToAdd];
|
|
178
|
+
updateCustomSetting(section, key, nextValues);
|
|
179
|
+
setDomainInputs(prev => ({
|
|
180
|
+
...prev,
|
|
181
|
+
[fieldId]: ''
|
|
182
|
+
}));
|
|
183
|
+
}, [customSettings, domainInputs, updateCustomSetting]);
|
|
184
|
+
const removeDomainValue = React.useCallback((fieldId, valueToRemove) => {
|
|
185
|
+
const { section, key } = DOMAIN_FIELD_MAP[fieldId];
|
|
186
|
+
const currentValues = toStringArray(toRecord(customSettings[section])[key]);
|
|
187
|
+
const nextValues = currentValues.filter(value => value !== valueToRemove);
|
|
188
|
+
updateCustomSetting(section, key, nextValues.length > 0 ? nextValues : undefined);
|
|
189
|
+
}, [customSettings, updateCustomSetting]);
|
|
190
|
+
const renderDomainList = React.useCallback((fieldId, label, placeholder, values) => {
|
|
191
|
+
const domainValues = toStringArray(values);
|
|
192
|
+
return (React.createElement(Box, null,
|
|
193
|
+
React.createElement(Typography, { variant: "body2", gutterBottom: true }, label),
|
|
194
|
+
React.createElement(List, { dense: true, sx: {
|
|
195
|
+
mb: 1,
|
|
196
|
+
maxHeight: 160,
|
|
197
|
+
overflow: 'auto',
|
|
198
|
+
border: 1,
|
|
199
|
+
borderColor: 'divider',
|
|
200
|
+
borderRadius: 1
|
|
201
|
+
} }, domainValues.length === 0 ? (React.createElement(ListItem, null,
|
|
202
|
+
React.createElement(ListItemText, { secondary: trans.__('No domains added.'), slotProps: {
|
|
203
|
+
secondary: {
|
|
204
|
+
color: 'text.secondary'
|
|
205
|
+
}
|
|
206
|
+
} }))) : (domainValues.map(value => (React.createElement(ListItem, { key: value, secondaryAction: React.createElement(IconButton, { onClick: () => removeDomainValue(fieldId, value), size: "small" },
|
|
207
|
+
React.createElement(Delete, { fontSize: "small" })) },
|
|
208
|
+
React.createElement(ListItemText, { primary: value })))))),
|
|
209
|
+
React.createElement(TextField, { fullWidth: true, size: "small", label: trans.__('Add Domain'), value: domainInputs[fieldId], onChange: e => setDomainInputs(prev => ({
|
|
210
|
+
...prev,
|
|
211
|
+
[fieldId]: e.target.value
|
|
212
|
+
})), onKeyDown: e => {
|
|
213
|
+
if (e.key === 'Enter') {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
addDomainValue(fieldId);
|
|
216
|
+
}
|
|
217
|
+
}, placeholder: placeholder, helperText: trans.__('Press Enter to add one domain.') })));
|
|
218
|
+
}, [addDomainValue, domainInputs, removeDomainValue, trans]);
|
|
69
219
|
const handleSave = () => {
|
|
70
220
|
if (!name.trim() || !provider || !model) {
|
|
71
221
|
return;
|
|
72
222
|
}
|
|
73
223
|
// Only include parameters if at least one is set
|
|
74
224
|
const hasParameters = Object.keys(parameters).some(key => parameters[key] !== undefined);
|
|
225
|
+
const sanitizedCustomSettings = sanitizeCustomSettingsForProvider(customSettings, providerToolCapabilities);
|
|
75
226
|
const config = {
|
|
76
227
|
name: name.trim(),
|
|
77
228
|
provider: provider,
|
|
78
229
|
model,
|
|
79
230
|
...(apiKey && { apiKey }),
|
|
80
231
|
...(baseURL && { baseURL }),
|
|
81
|
-
...(hasParameters && { parameters })
|
|
232
|
+
...(hasParameters && { parameters }),
|
|
233
|
+
...(Object.keys(sanitizedCustomSettings).length > 0 && {
|
|
234
|
+
customSettings: sanitizedCustomSettings
|
|
235
|
+
})
|
|
82
236
|
};
|
|
83
237
|
onSave(config);
|
|
84
238
|
onClose();
|
|
@@ -177,7 +331,56 @@ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mod
|
|
|
177
331
|
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: parameters.useFilterText ?? false, onChange: e => setParameters({
|
|
178
332
|
...parameters,
|
|
179
333
|
useFilterText: e.target.checked
|
|
180
|
-
}) }), label: trans.__('Use filter text') })
|
|
334
|
+
}) }), label: trans.__('Use filter text') }),
|
|
335
|
+
(supportsWebSearch || supportsWebFetch) && (React.createElement(React.Fragment, null,
|
|
336
|
+
React.createElement(Typography, { variant: "body2", color: "text.secondary", sx: { mt: 2, mb: 1 } }, trans.__('Provider Web Tools')),
|
|
337
|
+
supportsWebSearch && (React.createElement(React.Fragment, null,
|
|
338
|
+
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webSearchSettings.enabled === true, onChange: e => updateCustomSetting('webSearch', 'enabled', e.target.checked) }), label: trans.__('Enable Web Search') }),
|
|
339
|
+
webSearchSettings.enabled === true && (React.createElement(Box, { sx: {
|
|
340
|
+
pl: 2,
|
|
341
|
+
borderLeft: 2,
|
|
342
|
+
borderColor: 'divider',
|
|
343
|
+
display: 'flex',
|
|
344
|
+
flexDirection: 'column',
|
|
345
|
+
gap: 1.5
|
|
346
|
+
} },
|
|
347
|
+
(webSearchImplementation === 'openai' ||
|
|
348
|
+
webSearchImplementation === 'anthropic') &&
|
|
349
|
+
renderDomainList('webSearch.allowedDomains', trans.__('Allowed Domains'), trans.__('example.com'), webSearchSettings.allowedDomains),
|
|
350
|
+
webSearchImplementation === 'openai' && (React.createElement(React.Fragment, null,
|
|
351
|
+
React.createElement(FormControl, { fullWidth: true },
|
|
352
|
+
React.createElement(InputLabel, null, trans.__('Search Context Size')),
|
|
353
|
+
React.createElement(Select, { value: webSearchSettings.searchContextSize ??
|
|
354
|
+
'medium', label: trans.__('Search Context Size'), onChange: e => updateCustomSetting('webSearch', 'searchContextSize', e.target.value) },
|
|
355
|
+
React.createElement(MenuItem, { value: "low" }, trans.__('Low')),
|
|
356
|
+
React.createElement(MenuItem, { value: "medium" }, trans.__('Medium')),
|
|
357
|
+
React.createElement(MenuItem, { value: "high" }, trans.__('High')))),
|
|
358
|
+
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webSearchSettings.externalWebAccess !==
|
|
359
|
+
false, onChange: e => updateCustomSetting('webSearch', 'externalWebAccess', e.target.checked) }), label: trans.__('Use External Web Access') }))),
|
|
360
|
+
webSearchImplementation === 'anthropic' && (React.createElement(React.Fragment, null,
|
|
361
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Web Search Max Uses'), type: "number", value: webSearchSettings.maxUses ?? '', onChange: e => updateCustomSetting('webSearch', 'maxUses', e.target.value
|
|
362
|
+
? Number(e.target.value)
|
|
363
|
+
: undefined), inputProps: { min: 1 } }),
|
|
364
|
+
renderDomainList('webSearch.blockedDomains', trans.__('Blocked Domains'), trans.__('spam.example.com'), webSearchSettings.blockedDomains))))))),
|
|
365
|
+
supportsWebFetch && (React.createElement(React.Fragment, null,
|
|
366
|
+
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webFetchSettings.enabled === true, onChange: e => updateCustomSetting('webFetch', 'enabled', e.target.checked) }), label: trans.__('Enable Web Fetch') }),
|
|
367
|
+
webFetchSettings.enabled === true && (React.createElement(Box, { sx: {
|
|
368
|
+
pl: 2,
|
|
369
|
+
borderLeft: 2,
|
|
370
|
+
borderColor: 'divider',
|
|
371
|
+
display: 'flex',
|
|
372
|
+
flexDirection: 'column',
|
|
373
|
+
gap: 1.5
|
|
374
|
+
} },
|
|
375
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Web Fetch Max Uses'), type: "number", value: webFetchSettings.maxUses ?? '', onChange: e => updateCustomSetting('webFetch', 'maxUses', e.target.value
|
|
376
|
+
? Number(e.target.value)
|
|
377
|
+
: undefined), inputProps: { min: 1 } }),
|
|
378
|
+
React.createElement(TextField, { fullWidth: true, label: trans.__('Web Fetch Max Content Tokens'), type: "number", value: webFetchSettings.maxContentTokens ?? '', onChange: e => updateCustomSetting('webFetch', 'maxContentTokens', e.target.value
|
|
379
|
+
? Number(e.target.value)
|
|
380
|
+
: undefined), inputProps: { min: 1 } }),
|
|
381
|
+
renderDomainList('webFetch.allowedDomains', trans.__('Allowed Domains'), trans.__('docs.example.com'), webFetchSettings.allowedDomains),
|
|
382
|
+
renderDomainList('webFetch.blockedDomains', trans.__('Blocked Domains'), trans.__('spam.example.com'), webFetchSettings.blockedDomains),
|
|
383
|
+
React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: webFetchSettings.citationsEnabled === true, onChange: e => updateCustomSetting('webFetch', 'citationsEnabled', e.target.checked) }), label: trans.__('Enable Citations') })))))))))))),
|
|
181
384
|
React.createElement(DialogActions, null,
|
|
182
385
|
React.createElement(Button, { onClick: onClose }, trans.__('Cancel')),
|
|
183
386
|
React.createElement(Button, { onClick: handleSave, variant: "contained", disabled: !isValid }, mode === 'add' ? trans.__('Add Provider') : trans.__('Save Changes')))));
|