@oceanum/oceanumlab 4.6.2 → 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/DatameshWidget.d.ts +7 -1
- package/lib/DatameshWidget.js +110 -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.js +2 -10
- package/lib/httpUrl.d.ts +9 -0
- package/lib/httpUrl.js +20 -0
- package/lib/index.js +15 -2
- package/package.json +1 -1
- package/schema/datamesh-connect.json +6 -0
- package/src/DatameshWidget.tsx +184 -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/aiBackend.ts +150 -0
- package/src/chatRouter.ts +36 -8
- package/src/constants.ts +1 -1
- package/src/datameshUiUrl.ts +2 -9
- package/src/httpUrl.ts +23 -0
- package/src/index.ts +19 -2
|
@@ -24,6 +24,12 @@
|
|
|
24
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
25
|
"default": "https://ui.datamesh.oceanum.io"
|
|
26
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
|
+
},
|
|
27
33
|
"autoRunCode": {
|
|
28
34
|
"type": "boolean",
|
|
29
35
|
"title": "Auto-run code",
|
package/src/DatameshWidget.tsx
CHANGED
|
@@ -14,14 +14,23 @@ 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';
|
|
25
34
|
import { isDatameshUiMessage } from './datameshUiUrl';
|
|
26
35
|
import { describeProgress, onProgress, type Progress } from './progress';
|
|
27
36
|
|
|
@@ -460,38 +469,122 @@ class DatameshWorkspaceDisplay extends React.Component<IDatameshWorkspaceProps>
|
|
|
460
469
|
}
|
|
461
470
|
}
|
|
462
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
|
+
|
|
463
513
|
/**
|
|
464
|
-
*
|
|
465
|
-
*
|
|
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
|
+
|
|
550
|
+
/**
|
|
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.
|
|
466
554
|
*/
|
|
467
555
|
function TokenConfigMessage({
|
|
468
|
-
commands
|
|
556
|
+
commands,
|
|
557
|
+
settings
|
|
469
558
|
}: {
|
|
470
559
|
commands: CommandRegistry;
|
|
560
|
+
settings: IAiSettings;
|
|
471
561
|
}): React.ReactElement | null {
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
React.useEffect(() => {
|
|
475
|
-
const checkToken = () => setHasToken(!!window.datameshToken);
|
|
476
|
-
const interval = setInterval(checkToken, 1000);
|
|
477
|
-
return () => clearInterval(interval);
|
|
478
|
-
}, []);
|
|
562
|
+
const access = useAiAccess(commands, settings);
|
|
479
563
|
|
|
480
|
-
if (
|
|
564
|
+
if (access.source) {
|
|
481
565
|
return null;
|
|
482
566
|
}
|
|
483
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
|
+
|
|
484
584
|
return (
|
|
485
585
|
<div className="oceanum-token-config">
|
|
486
|
-
Set your{
|
|
487
|
-
|
|
488
|
-
onClick={() =>
|
|
489
|
-
commands.execute('settingeditor:open', { query: 'Oceanum' })
|
|
490
|
-
}
|
|
491
|
-
>
|
|
492
|
-
Datamesh token
|
|
493
|
-
</a>{' '}
|
|
494
|
-
to enable Oceanum.oi services{' '}
|
|
586
|
+
Set your <a onClick={openSettings}>Datamesh token</a> to enable Oceanum.io
|
|
587
|
+
services{' '}
|
|
495
588
|
<a
|
|
496
589
|
href="https://home.oceanum.io/account"
|
|
497
590
|
target="_blank"
|
|
@@ -510,10 +603,12 @@ interface IChatMessage {
|
|
|
510
603
|
|
|
511
604
|
interface IAIChatPanelProps {
|
|
512
605
|
commands: CommandRegistry;
|
|
606
|
+
settings: IAiSettings;
|
|
513
607
|
}
|
|
514
608
|
|
|
515
609
|
function AIChatPanel({
|
|
516
|
-
commands
|
|
610
|
+
commands,
|
|
611
|
+
settings
|
|
517
612
|
}: IAIChatPanelProps): React.ReactElement | null {
|
|
518
613
|
const [messages, setMessages] = React.useState<IChatMessage[]>([]);
|
|
519
614
|
const [input, setInput] = React.useState('');
|
|
@@ -532,55 +627,81 @@ function AIChatPanel({
|
|
|
532
627
|
const [historyIndex, setHistoryIndex] = React.useState(-1);
|
|
533
628
|
const [tempInput, setTempInput] = React.useState('');
|
|
534
629
|
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
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
|
+
: '';
|
|
538
639
|
|
|
539
640
|
// Check if code capability is enabled (null = loading/unknown)
|
|
540
641
|
const [codeEnabled, setCodeEnabled] = React.useState<boolean | null>(null);
|
|
541
642
|
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const currentToken = window.datameshToken || '';
|
|
546
|
-
if (currentToken !== token) {
|
|
547
|
-
setToken(currentToken);
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
const interval = setInterval(checkToken, 1000);
|
|
551
|
-
return () => clearInterval(interval);
|
|
552
|
-
}, [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);
|
|
553
646
|
|
|
554
|
-
// Fetch capabilities when
|
|
647
|
+
// Fetch capabilities when the credential or the address changes
|
|
555
648
|
React.useEffect(() => {
|
|
556
|
-
if (!
|
|
649
|
+
if (!access.source) {
|
|
557
650
|
setCodeEnabled(null);
|
|
558
651
|
return;
|
|
559
652
|
}
|
|
560
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
|
+
};
|
|
561
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
|
+
}
|
|
562
678
|
try {
|
|
563
|
-
const response = await fetch(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
);
|
|
679
|
+
const response = await fetch(`${access.backend}/api/capabilities`, {
|
|
680
|
+
headers: credential.headers
|
|
681
|
+
});
|
|
682
|
+
if (!current) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
571
685
|
if (response.ok) {
|
|
572
686
|
const data = await response.json();
|
|
573
|
-
|
|
687
|
+
if (current) {
|
|
688
|
+
setCodeEnabled(data.code === true);
|
|
689
|
+
}
|
|
574
690
|
} else {
|
|
575
691
|
setCodeEnabled(false);
|
|
576
692
|
}
|
|
577
693
|
} catch {
|
|
578
|
-
|
|
694
|
+
if (current) {
|
|
695
|
+
setCodeEnabled(false);
|
|
696
|
+
}
|
|
579
697
|
}
|
|
580
698
|
};
|
|
581
699
|
|
|
582
700
|
void fetchCapabilities();
|
|
583
|
-
|
|
701
|
+
return () => {
|
|
702
|
+
current = false;
|
|
703
|
+
};
|
|
704
|
+
}, [capabilitiesKey]);
|
|
584
705
|
|
|
585
706
|
// Auto-scroll to bottom when messages change
|
|
586
707
|
React.useEffect(() => {
|
|
@@ -722,8 +843,8 @@ function AIChatPanel({
|
|
|
722
843
|
}
|
|
723
844
|
};
|
|
724
845
|
|
|
725
|
-
// Hide AI panel if no
|
|
726
|
-
if (!
|
|
846
|
+
// Hide AI panel if no credential or code capability is not enabled
|
|
847
|
+
if (!access.source || codeEnabled === false) {
|
|
727
848
|
return null;
|
|
728
849
|
}
|
|
729
850
|
|
|
@@ -818,6 +939,12 @@ export interface IDatameshWidgetProps {
|
|
|
818
939
|
datameshUiUrl: () => string;
|
|
819
940
|
/** The Datamesh UI panel's iframe window; `null` when the panel is closed. */
|
|
820
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>;
|
|
821
948
|
commands: CommandRegistry;
|
|
822
949
|
getCurrentWidget: () => Widget;
|
|
823
950
|
}
|
|
@@ -915,10 +1042,13 @@ export class DatameshConnectWidget extends ReactWidget {
|
|
|
915
1042
|
this.renderDisplay(datameshWorkspace)
|
|
916
1043
|
}
|
|
917
1044
|
</UseSignal>
|
|
918
|
-
<TokenConfigMessage
|
|
1045
|
+
<TokenConfigMessage
|
|
1046
|
+
commands={this.props.commands}
|
|
1047
|
+
settings={this.props}
|
|
1048
|
+
/>
|
|
919
1049
|
</div>
|
|
920
1050
|
<div className="datamesh-connect-divider" />
|
|
921
|
-
<AIChatPanel commands={this.props.commands} />
|
|
1051
|
+
<AIChatPanel commands={this.props.commands} settings={this.props} />
|
|
922
1052
|
</div>
|
|
923
1053
|
);
|
|
924
1054
|
}
|