@oceanum/oceanumlab 4.6.1 → 4.7.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/DatameshUI.d.ts +10 -0
- package/lib/DatameshUI.js +20 -2
- package/lib/DatameshWidget.d.ts +11 -1
- package/lib/DatameshWidget.js +116 -43
- package/lib/aiBackend.d.ts +66 -0
- package/lib/aiBackend.js +101 -0
- package/lib/chatRouter.d.ts +5 -1
- package/lib/chatRouter.js +27 -9
- package/lib/constants.d.ts +1 -1
- package/lib/constants.js +1 -1
- package/lib/datameshUiUrl.d.ts +15 -0
- package/lib/datameshUiUrl.js +35 -0
- package/lib/httpUrl.d.ts +9 -0
- package/lib/httpUrl.js +20 -0
- package/lib/index.js +28 -7
- package/package.json +1 -1
- package/schema/datamesh-connect.json +12 -0
- package/src/DatameshUI.tsx +21 -2
- package/src/DatameshWidget.tsx +200 -54
- package/src/__tests__/DatameshWidget.spec.tsx +380 -0
- package/src/__tests__/aiBackend.spec.ts +272 -0
- package/src/__tests__/chatRouter.spec.ts +259 -17
- package/src/__tests__/datameshUiUrl.spec.ts +154 -0
- package/src/aiBackend.ts +150 -0
- package/src/chatRouter.ts +36 -8
- package/src/constants.ts +1 -1
- package/src/datameshUiUrl.ts +44 -0
- package/src/httpUrl.ts +23 -0
- package/src/index.ts +38 -10
package/lib/httpUrl.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An address typed into a setting, checked before anything uses it.
|
|
3
|
+
*
|
|
4
|
+
* A setting is typed in by hand, so anything that is not an http(s) URL falls
|
|
5
|
+
* back to the default rather than ending up in an iframe's `src` or a
|
|
6
|
+
* request's address.
|
|
7
|
+
*/
|
|
8
|
+
/** `value` as an http(s) URL, or `fallback` when it is empty or not one. */
|
|
9
|
+
export function validHttpUrl(value, fallback) {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = new URL((value !== null && value !== void 0 ? value : '').trim());
|
|
12
|
+
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
// Empty or not a URL: the default below.
|
|
18
|
+
}
|
|
19
|
+
return new URL(fallback);
|
|
20
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ILayoutRestorer, ILabStatus } from '@jupyterlab/application';
|
|
|
2
2
|
import { ICommandPalette, Notification } from '@jupyterlab/apputils';
|
|
3
3
|
import { LabIcon } from '@jupyterlab/ui-components';
|
|
4
4
|
import { find } from '@lumino/algorithm';
|
|
5
|
+
import { Signal } from '@lumino/signaling';
|
|
5
6
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
6
7
|
import { IStateDB } from '@jupyterlab/statedb';
|
|
7
8
|
import { INotebookTracker } from '@jupyterlab/notebook';
|
|
@@ -15,7 +16,7 @@ import { snapshotFromIpynb, snapshotOf } from './notebookContext';
|
|
|
15
16
|
import { notebookHost } from './notebookHost';
|
|
16
17
|
import { KernelHandoff } from './kernelHandoff';
|
|
17
18
|
import { runChatLoop, STOPPED } from './aiLoop';
|
|
18
|
-
import { MAX_OBSERVE_ROUNDS } from './constants';
|
|
19
|
+
import { DATAMESH_UI_SERVICE, MAX_OBSERVE_ROUNDS, OCEANUM_AI_BACKEND_URL } from './constants';
|
|
19
20
|
import '../style/index.css';
|
|
20
21
|
import oceanumSvg from '../style/icons/oceanum.svg';
|
|
21
22
|
// Theme-adaptive icon using currentColor (auto-adapts to light/dark themes)
|
|
@@ -39,6 +40,15 @@ export const datamesh_connect_extension = {
|
|
|
39
40
|
],
|
|
40
41
|
activate: (app, palette, restorer, status, settingRegistry) => {
|
|
41
42
|
console.log('Oceanum datamesh connect extension is loaded');
|
|
43
|
+
// The `datameshUiUrl` setting; the default until the settings load.
|
|
44
|
+
let datameshUiUrl = DATAMESH_UI_SERVICE;
|
|
45
|
+
// The `aiBackendUrl` and `datameshToken` settings for the AI chat; the
|
|
46
|
+
// defaults until the settings load.
|
|
47
|
+
let aiBackendUrl = OCEANUM_AI_BACKEND_URL;
|
|
48
|
+
let datameshTokenSetting = '';
|
|
49
|
+
// Tells the sidebar the settings above have changed.
|
|
50
|
+
const settingsChanged = new Signal(app);
|
|
51
|
+
const findDatameshUI = () => find(app.shell.widgets('main'), (widget) => widget.id === 'datamesh-ui');
|
|
42
52
|
//Try to get the datamesh token from the settings
|
|
43
53
|
const updateSettings = (set) => {
|
|
44
54
|
const token = set.get('datameshToken');
|
|
@@ -50,6 +60,15 @@ export const datamesh_connect_extension = {
|
|
|
50
60
|
}).then(res => console.log(res));
|
|
51
61
|
}
|
|
52
62
|
window.injectToken = set.get('injectToken').user;
|
|
63
|
+
aiBackendUrl = set.get('aiBackendUrl').composite;
|
|
64
|
+
datameshTokenSetting = set.get('datameshToken').composite;
|
|
65
|
+
// An open Datamesh UI panel follows a change of address.
|
|
66
|
+
datameshUiUrl = set.get('datameshUiUrl').composite;
|
|
67
|
+
const datameshUI = findDatameshUI();
|
|
68
|
+
if (datameshUI) {
|
|
69
|
+
datameshUI.url = datameshUiUrl;
|
|
70
|
+
}
|
|
71
|
+
settingsChanged.emit();
|
|
53
72
|
};
|
|
54
73
|
//Try to get the datamesh token from the envars
|
|
55
74
|
Promise.all([app.restored, settingRegistry.load(PLUGIN_ID)])
|
|
@@ -67,14 +86,11 @@ export const datamesh_connect_extension = {
|
|
|
67
86
|
};
|
|
68
87
|
const openDatameshUI = (event) => {
|
|
69
88
|
const widgetId = 'datamesh-ui';
|
|
70
|
-
|
|
71
|
-
return widget.id === widgetId;
|
|
72
|
-
});
|
|
73
|
-
if (openWidget) {
|
|
89
|
+
if (findDatameshUI()) {
|
|
74
90
|
app.shell.activateById(widgetId);
|
|
75
91
|
return;
|
|
76
92
|
}
|
|
77
|
-
const datameshUIWidget = new DatameshUI();
|
|
93
|
+
const datameshUIWidget = new DatameshUI(datameshUiUrl);
|
|
78
94
|
datameshUIWidget.title.label = 'Oceanum Datamesh';
|
|
79
95
|
datameshUIWidget.id = widgetId;
|
|
80
96
|
datameshUIWidget.title.closable = true;
|
|
@@ -87,6 +103,11 @@ export const datamesh_connect_extension = {
|
|
|
87
103
|
name: 'Datamesh Connect',
|
|
88
104
|
icon: oceanumIcon,
|
|
89
105
|
openDatameshUI: openDatameshUI,
|
|
106
|
+
datameshUiUrl: () => datameshUiUrl,
|
|
107
|
+
datameshUiFrame: () => { var _a, _b; return (_b = (_a = findDatameshUI()) === null || _a === void 0 ? void 0 : _a.frame) !== null && _b !== void 0 ? _b : null; },
|
|
108
|
+
datameshToken: () => datameshTokenSetting,
|
|
109
|
+
aiBackendUrl: () => aiBackendUrl,
|
|
110
|
+
settingsChanged,
|
|
90
111
|
commands: app.commands,
|
|
91
112
|
getCurrentWidget
|
|
92
113
|
});
|
|
@@ -129,7 +150,7 @@ export const oceanum_ai_extension = {
|
|
|
129
150
|
// The panel the current request read its selected cell from, if the
|
|
130
151
|
// notebook was open: a code answer may replace that cell only there.
|
|
131
152
|
let readFrom = null;
|
|
132
|
-
const router = new ChatRouter(settings, async () => {
|
|
153
|
+
const router = new ChatRouter(settings, app.commands, async () => {
|
|
133
154
|
const open = await pin.current();
|
|
134
155
|
readFrom = open;
|
|
135
156
|
if (open) {
|
package/package.json
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
"description": "Inject Datamesh token into code.",
|
|
19
19
|
"default": false
|
|
20
20
|
},
|
|
21
|
+
"datameshUiUrl": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"title": "Datamesh UI URL",
|
|
24
|
+
"description": "Address of the Datamesh UI shown in the Datamesh panel. Use the address it is finally served from: the panel only accepts workspace updates from this address's origin, so an address that redirects to another origin will not update the workspace.",
|
|
25
|
+
"default": "https://ui.datamesh.oceanum.io"
|
|
26
|
+
},
|
|
27
|
+
"aiBackendUrl": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"title": "Oceanum AI URL",
|
|
30
|
+
"description": "Address of the Oceanum AI backend used by the AI chat.",
|
|
31
|
+
"default": "https://ai.oceanum.io"
|
|
32
|
+
},
|
|
21
33
|
"autoRunCode": {
|
|
22
34
|
"type": "boolean",
|
|
23
35
|
"title": "Auto-run code",
|
package/src/DatameshUI.tsx
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ReactWidget } from '@jupyterlab/apputils';
|
|
3
|
-
import {
|
|
3
|
+
import { datameshUiSrc } from './datameshUiUrl';
|
|
4
4
|
|
|
5
5
|
export class DatameshUI extends ReactWidget {
|
|
6
|
+
/** `url` is the `datameshUiUrl` setting. */
|
|
7
|
+
constructor(private _url: string) {
|
|
8
|
+
super();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Point the panel at the `datameshUiUrl` setting's new value. The iframe
|
|
13
|
+
* reloads only if its address actually changes.
|
|
14
|
+
*/
|
|
15
|
+
set url(url: string) {
|
|
16
|
+
this._url = url;
|
|
17
|
+
this.update();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The iframe's window, for telling its messages apart; `null` if none. */
|
|
21
|
+
get frame(): Window | null {
|
|
22
|
+
return this.node.querySelector('iframe')?.contentWindow ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
render(): React.ReactElement {
|
|
7
26
|
return (
|
|
8
27
|
<iframe
|
|
9
|
-
src={
|
|
28
|
+
src={datameshUiSrc(this._url)}
|
|
10
29
|
className="datamesh-iframe"
|
|
11
30
|
></iframe>
|
|
12
31
|
);
|
package/src/DatameshWidget.tsx
CHANGED
|
@@ -14,14 +14,24 @@ import { LabIcon, addIcon } from '@jupyterlab/ui-components';
|
|
|
14
14
|
import { CommandRegistry } from '@lumino/commands';
|
|
15
15
|
import { MimeData, ReadonlyJSONArray } from '@lumino/coreutils';
|
|
16
16
|
import { Drag } from '@lumino/dragdrop';
|
|
17
|
-
import { Signal } from '@lumino/signaling';
|
|
17
|
+
import { ISignal, Signal } from '@lumino/signaling';
|
|
18
18
|
import { Widget } from '@lumino/widgets';
|
|
19
19
|
|
|
20
20
|
import React from 'react';
|
|
21
21
|
import { marked } from 'marked';
|
|
22
22
|
|
|
23
23
|
import { DatasourceItem } from './DatasourceItem';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
SIGN_IN_COMMAND,
|
|
26
|
+
aiBackendUrl,
|
|
27
|
+
aiCredentialSource,
|
|
28
|
+
canSignIn,
|
|
29
|
+
pastedToken,
|
|
30
|
+
resolveAiCredential,
|
|
31
|
+
signInToken,
|
|
32
|
+
type AiCredentialSource
|
|
33
|
+
} from './aiBackend';
|
|
34
|
+
import { isDatameshUiMessage } from './datameshUiUrl';
|
|
25
35
|
import { describeProgress, onProgress, type Progress } from './progress';
|
|
26
36
|
|
|
27
37
|
// Configure marked for safe rendering
|
|
@@ -459,38 +469,122 @@ class DatameshWorkspaceDisplay extends React.Component<IDatameshWorkspaceProps>
|
|
|
459
469
|
}
|
|
460
470
|
}
|
|
461
471
|
|
|
472
|
+
/** The settings the sidebar reaches Oceanum AI with. */
|
|
473
|
+
interface IAiSettings {
|
|
474
|
+
/**
|
|
475
|
+
* The `datameshToken` setting: the same value a chat request is signed with.
|
|
476
|
+
* Not `window.datameshToken`, which keeps a token after it is cleared.
|
|
477
|
+
*/
|
|
478
|
+
datameshToken: () => string;
|
|
479
|
+
/** The `aiBackendUrl` setting. */
|
|
480
|
+
aiBackendUrl: () => string;
|
|
481
|
+
/** Emitted when the settings change. */
|
|
482
|
+
settingsChanged: ISignal<unknown, void>;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* How the sidebar can reach Oceanum AI right now. Holds no sign-in token:
|
|
487
|
+
* finding one out means running the host's command.
|
|
488
|
+
*/
|
|
489
|
+
interface IAiAccess {
|
|
490
|
+
/** Which credential a request would carry; null when there is none. */
|
|
491
|
+
source: AiCredentialSource | null;
|
|
492
|
+
/** The pasted Datamesh token, trimmed; '' when there is none. */
|
|
493
|
+
pasted: string;
|
|
494
|
+
/** Whether the host can sign the user in to Oceanum.io on this site. */
|
|
495
|
+
canSignIn: boolean;
|
|
496
|
+
/** The backend address, from the `aiBackendUrl` setting. */
|
|
497
|
+
backend: string;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function readAiAccess(
|
|
501
|
+
commands: CommandRegistry,
|
|
502
|
+
settings: IAiSettings
|
|
503
|
+
): IAiAccess {
|
|
504
|
+
const datameshToken = settings.datameshToken();
|
|
505
|
+
return {
|
|
506
|
+
source: aiCredentialSource(datameshToken, commands),
|
|
507
|
+
pasted: pastedToken(datameshToken),
|
|
508
|
+
canSignIn: canSignIn(commands),
|
|
509
|
+
backend: aiBackendUrl(settings.aiBackendUrl())
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* How the sidebar can reach Oceanum AI, read again whenever the settings or the
|
|
515
|
+
* commands change: the host calls `notifyCommandChanged` when the user signs
|
|
516
|
+
* in or out.
|
|
517
|
+
*
|
|
518
|
+
* Only the registry's queries are used, never `execute`, and nothing runs on a
|
|
519
|
+
* timer: every executed command closes JupyterLab's command palette.
|
|
520
|
+
*/
|
|
521
|
+
function useAiAccess(
|
|
522
|
+
commands: CommandRegistry,
|
|
523
|
+
settings: IAiSettings
|
|
524
|
+
): IAiAccess {
|
|
525
|
+
const [access, setAccess] = React.useState(() =>
|
|
526
|
+
readAiAccess(commands, settings)
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
React.useEffect(() => {
|
|
530
|
+
const update = () => {
|
|
531
|
+
const next = readAiAccess(commands, settings);
|
|
532
|
+
// Same content keeps the same object, so nothing downstream re-runs.
|
|
533
|
+
setAccess(prev =>
|
|
534
|
+
JSON.stringify(prev) === JSON.stringify(next) ? prev : next
|
|
535
|
+
);
|
|
536
|
+
};
|
|
537
|
+
// Anything that changed between the first render and now.
|
|
538
|
+
update();
|
|
539
|
+
commands.commandChanged.connect(update);
|
|
540
|
+
settings.settingsChanged.connect(update);
|
|
541
|
+
return () => {
|
|
542
|
+
commands.commandChanged.disconnect(update);
|
|
543
|
+
settings.settingsChanged.disconnect(update);
|
|
544
|
+
};
|
|
545
|
+
}, [commands, settings]);
|
|
546
|
+
|
|
547
|
+
return access;
|
|
548
|
+
}
|
|
549
|
+
|
|
462
550
|
/**
|
|
463
|
-
* Shows a message prompting user to
|
|
464
|
-
*
|
|
551
|
+
* Shows a message prompting the user to sign in to Oceanum.io, or to configure
|
|
552
|
+
* their Datamesh token where the site has no sign-in. Only renders when there
|
|
553
|
+
* is no credential.
|
|
465
554
|
*/
|
|
466
555
|
function TokenConfigMessage({
|
|
467
|
-
commands
|
|
556
|
+
commands,
|
|
557
|
+
settings
|
|
468
558
|
}: {
|
|
469
559
|
commands: CommandRegistry;
|
|
560
|
+
settings: IAiSettings;
|
|
470
561
|
}): React.ReactElement | null {
|
|
471
|
-
const
|
|
562
|
+
const access = useAiAccess(commands, settings);
|
|
472
563
|
|
|
473
|
-
|
|
474
|
-
const checkToken = () => setHasToken(!!window.datameshToken);
|
|
475
|
-
const interval = setInterval(checkToken, 1000);
|
|
476
|
-
return () => clearInterval(interval);
|
|
477
|
-
}, []);
|
|
478
|
-
|
|
479
|
-
if (hasToken) {
|
|
564
|
+
if (access.source) {
|
|
480
565
|
return null;
|
|
481
566
|
}
|
|
482
567
|
|
|
568
|
+
const openSettings = () =>
|
|
569
|
+
commands.execute('settingeditor:open', { query: 'Oceanum' });
|
|
570
|
+
|
|
571
|
+
if (access.canSignIn) {
|
|
572
|
+
const signIn = () =>
|
|
573
|
+
commands.execute(SIGN_IN_COMMAND).catch(() => {
|
|
574
|
+
console.warn('Oceanum AI: could not start signing in to Oceanum.io.');
|
|
575
|
+
});
|
|
576
|
+
return (
|
|
577
|
+
<div className="oceanum-token-config">
|
|
578
|
+
<a onClick={() => void signIn()}>Sign in to Oceanum.io</a> to use
|
|
579
|
+
Oceanum AI, or set a <a onClick={openSettings}>Datamesh token</a>.
|
|
580
|
+
</div>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
483
584
|
return (
|
|
484
585
|
<div className="oceanum-token-config">
|
|
485
|
-
Set your{
|
|
486
|
-
|
|
487
|
-
onClick={() =>
|
|
488
|
-
commands.execute('settingeditor:open', { query: 'Oceanum' })
|
|
489
|
-
}
|
|
490
|
-
>
|
|
491
|
-
Datamesh token
|
|
492
|
-
</a>{' '}
|
|
493
|
-
to enable Oceanum.oi services{' '}
|
|
586
|
+
Set your <a onClick={openSettings}>Datamesh token</a> to enable Oceanum.io
|
|
587
|
+
services{' '}
|
|
494
588
|
<a
|
|
495
589
|
href="https://home.oceanum.io/account"
|
|
496
590
|
target="_blank"
|
|
@@ -509,10 +603,12 @@ interface IChatMessage {
|
|
|
509
603
|
|
|
510
604
|
interface IAIChatPanelProps {
|
|
511
605
|
commands: CommandRegistry;
|
|
606
|
+
settings: IAiSettings;
|
|
512
607
|
}
|
|
513
608
|
|
|
514
609
|
function AIChatPanel({
|
|
515
|
-
commands
|
|
610
|
+
commands,
|
|
611
|
+
settings
|
|
516
612
|
}: IAIChatPanelProps): React.ReactElement | null {
|
|
517
613
|
const [messages, setMessages] = React.useState<IChatMessage[]>([]);
|
|
518
614
|
const [input, setInput] = React.useState('');
|
|
@@ -531,55 +627,81 @@ function AIChatPanel({
|
|
|
531
627
|
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
532
628
|
const [tempInput, setTempInput] = React.useState('');
|
|
533
629
|
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
const
|
|
630
|
+
// Which credential and address the chat has, as the settings and the host
|
|
631
|
+
// report them.
|
|
632
|
+
const access = useAiAccess(commands, settings);
|
|
633
|
+
// What the capabilities depend on: which credential, the pasted token and
|
|
634
|
+
// the address. Not the sign-in token itself: it refreshes, and finding it out
|
|
635
|
+
// runs the host's command.
|
|
636
|
+
const capabilitiesKey = access.source
|
|
637
|
+
? JSON.stringify([access.source, access.pasted, access.backend])
|
|
638
|
+
: '';
|
|
537
639
|
|
|
538
640
|
// Check if code capability is enabled (null = loading/unknown)
|
|
539
641
|
const [codeEnabled, setCodeEnabled] = React.useState<boolean | null>(null);
|
|
540
642
|
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const currentToken = window.datameshToken || '';
|
|
545
|
-
if (currentToken !== token) {
|
|
546
|
-
setToken(currentToken);
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
const interval = setInterval(checkToken, 1000);
|
|
550
|
-
return () => clearInterval(interval);
|
|
551
|
-
}, [token]);
|
|
643
|
+
// The sidebar's one pending access-token request, shared rather than
|
|
644
|
+
// repeated if the capabilities are asked for again before it answers.
|
|
645
|
+
const tokenRequest = React.useRef<Promise<unknown> | null>(null);
|
|
552
646
|
|
|
553
|
-
// Fetch capabilities when
|
|
647
|
+
// Fetch capabilities when the credential or the address changes
|
|
554
648
|
React.useEffect(() => {
|
|
555
|
-
if (!
|
|
649
|
+
if (!access.source) {
|
|
556
650
|
setCodeEnabled(null);
|
|
557
651
|
return;
|
|
558
652
|
}
|
|
559
653
|
|
|
654
|
+
// An answer for a credential that has since changed is dropped.
|
|
655
|
+
let current = true;
|
|
656
|
+
const signInTokenOnce = (): Promise<unknown> => {
|
|
657
|
+
if (!tokenRequest.current) {
|
|
658
|
+
tokenRequest.current = signInToken(commands)().finally(() => {
|
|
659
|
+
tokenRequest.current = null;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return tokenRequest.current;
|
|
663
|
+
};
|
|
560
664
|
const fetchCapabilities = async () => {
|
|
665
|
+
const credential = await resolveAiCredential(
|
|
666
|
+
access.pasted,
|
|
667
|
+
signInTokenOnce
|
|
668
|
+
);
|
|
669
|
+
if (!current) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (!credential) {
|
|
673
|
+
// The host says signed in but has no token to give: leave it to a
|
|
674
|
+
// request to report, rather than hide the chat.
|
|
675
|
+
setCodeEnabled(null);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
561
678
|
try {
|
|
562
|
-
const response = await fetch(
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
);
|
|
679
|
+
const response = await fetch(`${access.backend}/api/capabilities`, {
|
|
680
|
+
headers: credential.headers
|
|
681
|
+
});
|
|
682
|
+
if (!current) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
570
685
|
if (response.ok) {
|
|
571
686
|
const data = await response.json();
|
|
572
|
-
|
|
687
|
+
if (current) {
|
|
688
|
+
setCodeEnabled(data.code === true);
|
|
689
|
+
}
|
|
573
690
|
} else {
|
|
574
691
|
setCodeEnabled(false);
|
|
575
692
|
}
|
|
576
693
|
} catch {
|
|
577
|
-
|
|
694
|
+
if (current) {
|
|
695
|
+
setCodeEnabled(false);
|
|
696
|
+
}
|
|
578
697
|
}
|
|
579
698
|
};
|
|
580
699
|
|
|
581
700
|
void fetchCapabilities();
|
|
582
|
-
|
|
701
|
+
return () => {
|
|
702
|
+
current = false;
|
|
703
|
+
};
|
|
704
|
+
}, [capabilitiesKey]);
|
|
583
705
|
|
|
584
706
|
// Auto-scroll to bottom when messages change
|
|
585
707
|
React.useEffect(() => {
|
|
@@ -721,8 +843,8 @@ function AIChatPanel({
|
|
|
721
843
|
}
|
|
722
844
|
};
|
|
723
845
|
|
|
724
|
-
// Hide AI panel if no
|
|
725
|
-
if (!
|
|
846
|
+
// Hide AI panel if no credential or code capability is not enabled
|
|
847
|
+
if (!access.source || codeEnabled === false) {
|
|
726
848
|
return null;
|
|
727
849
|
}
|
|
728
850
|
|
|
@@ -813,6 +935,16 @@ export interface IDatameshWidgetProps {
|
|
|
813
935
|
name: string;
|
|
814
936
|
icon: LabIcon;
|
|
815
937
|
openDatameshUI: any;
|
|
938
|
+
/** The `datameshUiUrl` setting. */
|
|
939
|
+
datameshUiUrl: () => string;
|
|
940
|
+
/** The Datamesh UI panel's iframe window; `null` when the panel is closed. */
|
|
941
|
+
datameshUiFrame: () => Window | null;
|
|
942
|
+
/** The `datameshToken` setting. */
|
|
943
|
+
datameshToken: () => string;
|
|
944
|
+
/** The `aiBackendUrl` setting. */
|
|
945
|
+
aiBackendUrl: () => string;
|
|
946
|
+
/** Emitted when the settings change. */
|
|
947
|
+
settingsChanged: ISignal<unknown, void>;
|
|
816
948
|
commands: CommandRegistry;
|
|
817
949
|
getCurrentWidget: () => Widget;
|
|
818
950
|
}
|
|
@@ -841,6 +973,17 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
841
973
|
}
|
|
842
974
|
|
|
843
975
|
receiveIFrameMessage(event: MessageEvent): void {
|
|
976
|
+
// Any page or frame can post to this window; only the Datamesh UI panel
|
|
977
|
+
// may change the workspace.
|
|
978
|
+
if (
|
|
979
|
+
!isDatameshUiMessage(
|
|
980
|
+
event,
|
|
981
|
+
this.props.datameshUiUrl(),
|
|
982
|
+
this.props.datameshUiFrame()
|
|
983
|
+
)
|
|
984
|
+
) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
844
987
|
if (event.data && event.data.action === 'workspace-modify') {
|
|
845
988
|
this.datameshWorkspaceSpec = {
|
|
846
989
|
id: event.data.id,
|
|
@@ -899,10 +1042,13 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
899
1042
|
this.renderDisplay(datameshWorkspace)
|
|
900
1043
|
}
|
|
901
1044
|
</UseSignal>
|
|
902
|
-
<TokenConfigMessage
|
|
1045
|
+
<TokenConfigMessage
|
|
1046
|
+
commands={this.props.commands}
|
|
1047
|
+
settings={this.props}
|
|
1048
|
+
/>
|
|
903
1049
|
</div>
|
|
904
1050
|
<div className="datamesh-connect-divider" />
|
|
905
|
-
<AIChatPanel commands={this.props.commands} />
|
|
1051
|
+
<AIChatPanel commands={this.props.commands} settings={this.props} />
|
|
906
1052
|
</div>
|
|
907
1053
|
);
|
|
908
1054
|
}
|