@datalayer/agent-runtimes 1.3.90 → 1.3.91
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/words.d.ts +7 -0
- package/lib/chat/words.js +12 -0
- package/lib/components/a2ui/styles.js +16 -5
- package/lib/loop/apps/AppFeedback.js +12 -3
- package/lib/loop/apps/feedback.d.ts +3 -2
- package/lib/loop/apps/feedback.js +4 -2
- package/lib/loop/core/index.d.ts +19 -0
- package/lib/loop/core/index.js +27 -0
- package/lib/loop/plugins/app-computer/AppComputer.js +28 -7
- package/lib/loop/plugins/app-page/AppPage.js +25 -7
- package/lib/loop/plugins/chat/ChatView.js +49 -24
- package/package.json +1 -1
package/lib/chat/words.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export type ChatWords = {
|
|
|
20
20
|
demoKeyExpired: string;
|
|
21
21
|
/** The person's key has run out. */
|
|
22
22
|
keyExpired: string;
|
|
23
|
+
/**
|
|
24
|
+
* No runtime is assigned to the conversation on Datalayer (the pool had
|
|
25
|
+
* none, or its launch was refused), with why: nothing is sent.
|
|
26
|
+
*/
|
|
27
|
+
noRuntime: (reason: string) => string;
|
|
28
|
+
/** Its runtime is still being assigned: nothing is sent until it is. */
|
|
29
|
+
runtimeStarting: string;
|
|
23
30
|
/** The empty chat's title. */
|
|
24
31
|
startConversation: string;
|
|
25
32
|
/** The empty chat's line when it has no starters. */
|
package/lib/chat/words.js
CHANGED
|
@@ -30,6 +30,8 @@ export const CHAT_WORDS = {
|
|
|
30
30
|
suggestedPrompts: 'Suggested prompts',
|
|
31
31
|
demoKeyExpired: 'This demo runs on a shared key, and its time is up. Sign in to keep going.',
|
|
32
32
|
keyExpired: 'Your key has expired. Sign in to keep going.',
|
|
33
|
+
noRuntime: reason => `No runtime is assigned to this conversation, so nothing can be sent. ${reason}`,
|
|
34
|
+
runtimeStarting: 'Its runtime is being assigned: you can send once it is.',
|
|
33
35
|
startConversation: 'Start a conversation',
|
|
34
36
|
sendToBegin: 'Send a message to begin chatting',
|
|
35
37
|
copyTurn: 'Copy this turn',
|
|
@@ -99,6 +101,8 @@ export const CHAT_WORDS = {
|
|
|
99
101
|
suggestedPrompts: 'Suggestions de questions',
|
|
100
102
|
demoKeyExpired: 'Cette démo utilise une clé partagée, et son temps est écoulé. Connectez-vous pour continuer.',
|
|
101
103
|
keyExpired: 'Votre clé a expiré. Connectez-vous pour continuer.',
|
|
104
|
+
noRuntime: reason => `Aucun runtime n’est attribué à cette conversation : rien ne peut être envoyé. ${reason}`,
|
|
105
|
+
runtimeStarting: 'Son runtime est en cours d’attribution : vous pourrez envoyer une fois qu’il le sera.',
|
|
102
106
|
startConversation: 'Commencer une conversation',
|
|
103
107
|
sendToBegin: 'Envoyez un message pour commencer',
|
|
104
108
|
copyTurn: 'Copier cet échange',
|
|
@@ -170,6 +174,8 @@ export const CHAT_WORDS = {
|
|
|
170
174
|
suggestedPrompts: 'Preguntas sugeridas',
|
|
171
175
|
demoKeyExpired: 'Esta demo usa una clave compartida y su tiempo se ha agotado. Inicia sesión para continuar.',
|
|
172
176
|
keyExpired: 'Tu clave ha caducado. Inicia sesión para continuar.',
|
|
177
|
+
noRuntime: reason => `No hay ningún runtime asignado a esta conversación, así que no se puede enviar nada. ${reason}`,
|
|
178
|
+
runtimeStarting: 'Se le está asignando un runtime: podrás enviar cuando lo tenga.',
|
|
173
179
|
startConversation: 'Empieza una conversación',
|
|
174
180
|
sendToBegin: 'Envía un mensaje para empezar',
|
|
175
181
|
copyTurn: 'Copiar este turno',
|
|
@@ -241,6 +247,8 @@ export const CHAT_WORDS = {
|
|
|
241
247
|
suggestedPrompts: 'Vorgeschlagene Fragen',
|
|
242
248
|
demoKeyExpired: 'Diese Demo läuft mit einem geteilten Schlüssel, und ihre Zeit ist abgelaufen. Melde dich an, um weiterzumachen.',
|
|
243
249
|
keyExpired: 'Dein Schlüssel ist abgelaufen. Melde dich an, um weiterzumachen.',
|
|
250
|
+
noRuntime: reason => `Diesem Gespräch ist keine Runtime zugewiesen, daher kann nichts gesendet werden. ${reason}`,
|
|
251
|
+
runtimeStarting: 'Eine Runtime wird zugewiesen: Du kannst senden, sobald sie da ist.',
|
|
244
252
|
startConversation: 'Ein Gespräch beginnen',
|
|
245
253
|
sendToBegin: 'Sende eine Nachricht, um zu beginnen',
|
|
246
254
|
copyTurn: 'Diesen Austausch kopieren',
|
|
@@ -312,6 +320,8 @@ export const CHAT_WORDS = {
|
|
|
312
320
|
suggestedPrompts: 'Domande suggerite',
|
|
313
321
|
demoKeyExpired: 'Questa demo usa una chiave condivisa e il suo tempo è scaduto. Accedi per continuare.',
|
|
314
322
|
keyExpired: 'La tua chiave è scaduta. Accedi per continuare.',
|
|
323
|
+
noRuntime: reason => `Nessun runtime è assegnato a questa conversazione, quindi non si può inviare nulla. ${reason}`,
|
|
324
|
+
runtimeStarting: 'Gli si sta assegnando un runtime: potrai inviare appena sarà pronto.',
|
|
315
325
|
startConversation: 'Inizia una conversazione',
|
|
316
326
|
sendToBegin: 'Invia un messaggio per iniziare',
|
|
317
327
|
copyTurn: 'Copia questo scambio',
|
|
@@ -383,6 +393,8 @@ export const CHAT_WORDS = {
|
|
|
383
393
|
suggestedPrompts: 'Perguntas sugeridas',
|
|
384
394
|
demoKeyExpired: 'Esta demonstração usa uma chave compartilhada e o tempo dela acabou. Entre para continuar.',
|
|
385
395
|
keyExpired: 'Sua chave expirou. Entre para continuar.',
|
|
396
|
+
noRuntime: reason => `Nenhum runtime está atribuído a esta conversa, então nada pode ser enviado. ${reason}`,
|
|
397
|
+
runtimeStarting: 'Um runtime está sendo atribuído: você poderá enviar assim que estiver.',
|
|
386
398
|
startConversation: 'Comece uma conversa',
|
|
387
399
|
sendToBegin: 'Envie uma mensagem para começar',
|
|
388
400
|
copyTurn: 'Copiar esta troca',
|
|
@@ -36,21 +36,32 @@ export const A2UI_RENDER_SCOPE_SX = {
|
|
|
36
36
|
'& .a2ui-caption': {
|
|
37
37
|
color: 'var(--a2ui-text-caption-color, var(--a2ui-color-on-surface, #656d76))',
|
|
38
38
|
},
|
|
39
|
+
/*
|
|
40
|
+
* A button is the theme's filled button (STUDIO P-04, T-30): its fill and
|
|
41
|
+
* its label are Primer's primary-button properties, which every theme and
|
|
42
|
+
* an application's accent set together (`themeAccentVars`), so the label
|
|
43
|
+
* is always the accent's own text on it. The label is a Text inside the
|
|
44
|
+
* button, which the `.a2ui-text` rule above paints in the surface's ink —
|
|
45
|
+
* a dark label on Spatial's dark accent — so it inherits the button's.
|
|
46
|
+
*/
|
|
39
47
|
'& .a2ui-button': {
|
|
40
48
|
appearance: 'none',
|
|
41
49
|
cursor: 'pointer',
|
|
42
|
-
border: '1px solid var(--
|
|
43
|
-
background: 'var(--
|
|
44
|
-
color: 'var(--
|
|
50
|
+
border: '1px solid var(--button-primary-bgColor-rest)',
|
|
51
|
+
background: 'var(--button-primary-bgColor-rest)',
|
|
52
|
+
color: 'var(--button-primary-fgColor-rest)',
|
|
45
53
|
borderRadius: 'var(--a2ui-button-border-radius, 6px)',
|
|
46
54
|
padding: 'var(--a2ui-button-padding, 5px 16px)',
|
|
47
55
|
fontWeight: 'var(--a2ui-button-font-weight, 500)',
|
|
48
56
|
boxShadow: 'var(--a2ui-button-box-shadow, none)',
|
|
49
57
|
transition: 'background-color 0.15s ease, border-color 0.15s ease',
|
|
50
58
|
},
|
|
59
|
+
'& .a2ui-button .a2ui-text': {
|
|
60
|
+
color: 'inherit',
|
|
61
|
+
},
|
|
51
62
|
'& .a2ui-button:hover': {
|
|
52
|
-
background: 'var(--
|
|
53
|
-
borderColor: 'var(--
|
|
63
|
+
background: 'var(--button-primary-bgColor-hover)',
|
|
64
|
+
borderColor: 'var(--button-primary-bgColor-hover)',
|
|
54
65
|
},
|
|
55
66
|
'& .a2ui-button:disabled': {
|
|
56
67
|
opacity: 0.6,
|
|
@@ -5,7 +5,7 @@ import { ThumbsdownIcon, ThumbsupIcon } from '@primer/octicons-react';
|
|
|
5
5
|
import { contribution, definePlugin, signal } from '@datalayer/reactor';
|
|
6
6
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
7
7
|
import { useIAMStore } from '../../state';
|
|
8
|
-
import { LoopChatTurn, LoopPromptPanel, } from '../core';
|
|
8
|
+
import { LoopChatTurn, LoopPromptPanel, agentServerOf, } from '../core';
|
|
9
9
|
import { answerIdOf, FEEDBACK_WORDS, sendFeedback } from './feedback';
|
|
10
10
|
export const APP_FEEDBACK_PLUGIN_NAME = '@datalayer/loop-plugin-app-feedback';
|
|
11
11
|
/* A signal to read when no chat contributed a turn: the hook needs one. */
|
|
@@ -28,11 +28,20 @@ export function AppFeedback({ app, workspace, }) {
|
|
|
28
28
|
const thread = turn.thread;
|
|
29
29
|
const choice = liked?.turn === turn.id ? liked.liked : null;
|
|
30
30
|
const done = said?.turn === turn.id ? said : null;
|
|
31
|
-
|
|
31
|
+
// The runtime that answered: none, nothing is sent (no stand-in server).
|
|
32
|
+
const agentBaseUrl = agentServerOf(workspace.sandbox, workspace.serverUrl);
|
|
32
33
|
const send = async () => {
|
|
33
34
|
if (choice === null) {
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
37
|
+
if (agentBaseUrl === undefined) {
|
|
38
|
+
setSaid({
|
|
39
|
+
turn: turn.id,
|
|
40
|
+
state: 'refused',
|
|
41
|
+
says: FEEDBACK_WORDS.noRuntime,
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
36
45
|
setSaid({ turn: turn.id, state: 'sending', says: '' });
|
|
37
46
|
try {
|
|
38
47
|
await sendFeedback({
|
|
@@ -64,7 +73,7 @@ export function AppFeedback({ app, workspace, }) {
|
|
|
64
73
|
flexDirection: 'column',
|
|
65
74
|
gap: 4,
|
|
66
75
|
padding: '4px 8px',
|
|
67
|
-
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.ask }), _jsx(IconButton, { size: "small", variant: choice === true ? 'default' : 'invisible', icon: ThumbsupIcon, "aria-label": FEEDBACK_WORDS.liked, "aria-pressed": choice === true, onClick: () => setLiked({ turn: turn.id, liked: true }) }), _jsx(IconButton, { size: "small", variant: choice === false ? 'default' : 'invisible', icon: ThumbsdownIcon, "aria-label": FEEDBACK_WORDS.disliked, "aria-pressed": choice === false, onClick: () => setLiked({ turn: turn.id, liked: false }) })] }), choice !== null ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": FEEDBACK_WORDS.comment, placeholder: FEEDBACK_WORDS.comment, value: comment, rows: 2, resize: "vertical", block: true, onChange: event => setComment(event.target.value) }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Button, { size: "small", variant: "default", disabled: done?.state === 'sending', onClick: () => void send(), children: FEEDBACK_WORDS.send }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.where(app.name) })] })] })) : null, done?.state === 'refused' ? (
|
|
76
|
+
}, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.ask }), _jsx(IconButton, { size: "small", variant: choice === true ? 'default' : 'invisible', icon: ThumbsupIcon, "aria-label": FEEDBACK_WORDS.liked, "aria-pressed": choice === true, unsafeDisableTooltip: true, onClick: () => setLiked({ turn: turn.id, liked: true }) }), _jsx(IconButton, { size: "small", variant: choice === false ? 'default' : 'invisible', icon: ThumbsdownIcon, "aria-label": FEEDBACK_WORDS.disliked, "aria-pressed": choice === false, unsafeDisableTooltip: true, onClick: () => setLiked({ turn: turn.id, liked: false }) })] }), choice !== null ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": FEEDBACK_WORDS.comment, placeholder: FEEDBACK_WORDS.comment, value: comment, rows: 2, resize: "vertical", block: true, onChange: event => setComment(event.target.value) }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [_jsx(Button, { size: "small", variant: "default", disabled: done?.state === 'sending', onClick: () => void send(), children: FEEDBACK_WORDS.send }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: FEEDBACK_WORDS.where(app.name) })] })] })) : null, done?.state === 'refused' ? (
|
|
68
77
|
// Said in the ink, as an alert: not a verdict, so not its colour.
|
|
69
78
|
_jsx(Text, { role: "alert", sx: { fontSize: 0, color: 'fg.default' }, children: done.says })) : null] }));
|
|
70
79
|
}
|
|
@@ -32,13 +32,14 @@ export declare const answerIdOf: (answers: number) => string | undefined;
|
|
|
32
32
|
/** What the feedback strip says, in a person's words. */
|
|
33
33
|
export declare const FEEDBACK_WORDS: {
|
|
34
34
|
readonly ask: "Was this answer useful?";
|
|
35
|
-
readonly liked: "
|
|
36
|
-
readonly disliked: "
|
|
35
|
+
readonly liked: "This answer was useful";
|
|
36
|
+
readonly disliked: "This answer was not useful";
|
|
37
37
|
readonly comment: "Anything to add? (optional)";
|
|
38
38
|
readonly send: "Send";
|
|
39
39
|
readonly kept: (app: string) => string;
|
|
40
40
|
readonly where: (app: string) => string;
|
|
41
41
|
readonly tooLong: "A comment is at most 2000 characters.";
|
|
42
|
+
readonly noRuntime: "No runtime is assigned to this conversation, so nothing can be sent.";
|
|
42
43
|
readonly noSession: "This conversation has no name the application knows yet: send a message first.";
|
|
43
44
|
};
|
|
44
45
|
/** Whether an application keeps feedback: its record names it. */
|
|
@@ -9,13 +9,15 @@ export const answerIdOf = (answers) => answers > 0 ? `answer-${answers}` : undef
|
|
|
9
9
|
/** What the feedback strip says, in a person's words. */
|
|
10
10
|
export const FEEDBACK_WORDS = {
|
|
11
11
|
ask: 'Was this answer useful?',
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// The thumbs' names (P-24): what pressing one says of the answer.
|
|
13
|
+
liked: 'This answer was useful',
|
|
14
|
+
disliked: 'This answer was not useful',
|
|
14
15
|
comment: 'Anything to add? (optional)',
|
|
15
16
|
send: 'Send',
|
|
16
17
|
kept: (app) => `Thank you. Kept in ${app}’s record.`,
|
|
17
18
|
where: (app) => `What you say is kept with this conversation in ${app}’s record, for its builder to read.`,
|
|
18
19
|
tooLong: `A comment is at most ${FEEDBACK_COMMENT_LIMIT} characters.`,
|
|
20
|
+
noRuntime: 'No runtime is assigned to this conversation, so nothing can be sent.',
|
|
19
21
|
noSession: 'This conversation has no name the application knows yet: send a message first.',
|
|
20
22
|
};
|
|
21
23
|
/** Whether an application keeps feedback: its record names it. */
|
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ContextSnapshotData } from '../../types/context';
|
|
|
8
8
|
import type { AssistantCharacter } from '../../chat/assistant/characters';
|
|
9
9
|
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
10
10
|
import type { PresenceState } from '../../chat/presence/presenceStatus';
|
|
11
|
+
import type { ChatWords } from '../../chat/words';
|
|
11
12
|
/** Lifecycle of the sandbox a workspace is attached to. */
|
|
12
13
|
export type SandboxState = 'idle' | 'starting' | 'running' | 'stopping' | 'error';
|
|
13
14
|
/** What the workspace knows about the sandbox behind it. */
|
|
@@ -53,6 +54,24 @@ export type SandboxSnapshot = {
|
|
|
53
54
|
*/
|
|
54
55
|
agentBaseUrl?: string;
|
|
55
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* The agent-runtimes server a workspace's agent is on, or nothing while
|
|
59
|
+
* there is none.
|
|
60
|
+
*
|
|
61
|
+
* On Datalayer the agent is on its runtime and nowhere else: until one is
|
|
62
|
+
* assigned there is nothing to address, and the host's own server is not a
|
|
63
|
+
* stand-in — on a hosted page it is the page's own origin, which answered a
|
|
64
|
+
* question with a 404 while the pool had no runtime (STUDIO P-24, round 9).
|
|
65
|
+
* Every other target's agent is on the host's server, or on the one its
|
|
66
|
+
* sandbox names.
|
|
67
|
+
*/
|
|
68
|
+
export declare function agentServerOf(sandbox: Pick<SandboxSnapshot, 'target' | 'agentBaseUrl'>, serverUrl: string): string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* What is said while {@link agentServerOf} names no server: the runtime is
|
|
71
|
+
* being assigned, or — its launch refused, the pool empty — none is, and
|
|
72
|
+
* why. One sentence for the chat, the page and the computer alike.
|
|
73
|
+
*/
|
|
74
|
+
export declare function noRuntimeSaid(sandbox: Pick<SandboxSnapshot, 'state' | 'errorReason'>, words: Pick<ChatWords, 'noRuntime' | 'runtimeStarting'>): string;
|
|
56
75
|
/**
|
|
57
76
|
* What a view reads while the sandbox plugin is absent.
|
|
58
77
|
*
|
package/lib/loop/core/index.js
CHANGED
|
@@ -14,6 +14,33 @@
|
|
|
14
14
|
* @module loop/core
|
|
15
15
|
*/
|
|
16
16
|
import { defineContributionPoint, defineGate } from '@datalayer/reactor';
|
|
17
|
+
/**
|
|
18
|
+
* The agent-runtimes server a workspace's agent is on, or nothing while
|
|
19
|
+
* there is none.
|
|
20
|
+
*
|
|
21
|
+
* On Datalayer the agent is on its runtime and nowhere else: until one is
|
|
22
|
+
* assigned there is nothing to address, and the host's own server is not a
|
|
23
|
+
* stand-in — on a hosted page it is the page's own origin, which answered a
|
|
24
|
+
* question with a 404 while the pool had no runtime (STUDIO P-24, round 9).
|
|
25
|
+
* Every other target's agent is on the host's server, or on the one its
|
|
26
|
+
* sandbox names.
|
|
27
|
+
*/
|
|
28
|
+
export function agentServerOf(sandbox, serverUrl) {
|
|
29
|
+
if (sandbox.target === 'datalayer') {
|
|
30
|
+
return sandbox.agentBaseUrl || undefined;
|
|
31
|
+
}
|
|
32
|
+
return sandbox.agentBaseUrl || serverUrl;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What is said while {@link agentServerOf} names no server: the runtime is
|
|
36
|
+
* being assigned, or — its launch refused, the pool empty — none is, and
|
|
37
|
+
* why. One sentence for the chat, the page and the computer alike.
|
|
38
|
+
*/
|
|
39
|
+
export function noRuntimeSaid(sandbox, words) {
|
|
40
|
+
return sandbox.state === 'error'
|
|
41
|
+
? words.noRuntime(sandbox.errorReason ?? '')
|
|
42
|
+
: words.runtimeStarting;
|
|
43
|
+
}
|
|
17
44
|
/**
|
|
18
45
|
* What a view reads while the sandbox plugin is absent.
|
|
19
46
|
*
|
|
@@ -5,7 +5,8 @@ import { Box } from '@datalayer/primer-addons';
|
|
|
5
5
|
import { signal } from '@datalayer/reactor';
|
|
6
6
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
7
7
|
import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
|
|
8
|
-
import {
|
|
8
|
+
import { useChatWords } from '../../../chat/ChatLanguage';
|
|
9
|
+
import { LoopChatTurn, agentServerOf, noRuntimeSaid, IDLE_SANDBOX_SNAPSHOT, } from '../../core';
|
|
9
10
|
import { COMPUTER_PARTS, COMPUTER_WORDS, computerParts, downloadComputerFile, handBackComputer, hasComputer, listComputerFiles, parentOf, partsOff, readComputer, runOnComputer, takeOverComputer, terminalEntries, } from '../../apps/computer';
|
|
10
11
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
11
12
|
const NO_CONVERSATION = signal([]);
|
|
@@ -109,9 +110,23 @@ export function AppComputer({ app, workspace, }) {
|
|
|
109
110
|
setEnded(turn.id);
|
|
110
111
|
}
|
|
111
112
|
}, [settled, turn.id]);
|
|
112
|
-
const
|
|
113
|
+
const chatText = useChatWords();
|
|
114
|
+
/*
|
|
115
|
+
* The runtime its agent is on, or nothing while none is assigned: then the
|
|
116
|
+
* computer is asked nothing and says why, rather than asking the host's
|
|
117
|
+
* server (on a hosted page, the page's own origin). Outside a workspace
|
|
118
|
+
* there is none either.
|
|
119
|
+
*/
|
|
120
|
+
const serverUrl = workspace
|
|
121
|
+
? agentServerOf(workspace.sandbox, workspace.serverUrl)
|
|
122
|
+
: undefined;
|
|
123
|
+
const noRuntime = serverUrl === undefined
|
|
124
|
+
? noRuntimeSaid(workspace?.sandbox ?? IDLE_SANDBOX_SNAPSHOT, chatText)
|
|
125
|
+
: undefined;
|
|
113
126
|
const agentId = workspace?.agentId || app.id;
|
|
114
|
-
const context = useMemo(() =>
|
|
127
|
+
const context = useMemo(() => serverUrl === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: { serverUrl, agentId, token: token || undefined }, [serverUrl, agentId, token]);
|
|
115
130
|
const parts = computerParts(app);
|
|
116
131
|
const [state, setState] = useState();
|
|
117
132
|
const [error, setError] = useState('');
|
|
@@ -125,6 +140,9 @@ export function AppComputer({ app, workspace, }) {
|
|
|
125
140
|
};
|
|
126
141
|
}, []);
|
|
127
142
|
const read = useCallback(() => {
|
|
143
|
+
if (!context) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
128
146
|
readComputer(context).then(found => {
|
|
129
147
|
if (alive.current) {
|
|
130
148
|
setState(found);
|
|
@@ -135,11 +153,14 @@ export function AppComputer({ app, workspace, }) {
|
|
|
135
153
|
}, [context]);
|
|
136
154
|
const used = hasComputer(parts);
|
|
137
155
|
useEffect(() => {
|
|
138
|
-
if (used &&
|
|
156
|
+
if (used && context) {
|
|
139
157
|
read();
|
|
140
158
|
}
|
|
141
|
-
}, [used,
|
|
159
|
+
}, [used, context, read, ended]);
|
|
142
160
|
const act = (step) => {
|
|
161
|
+
if (!context) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
143
164
|
setBusy(true);
|
|
144
165
|
step(context).then(() => {
|
|
145
166
|
if (alive.current) {
|
|
@@ -154,10 +175,10 @@ export function AppComputer({ app, workspace, }) {
|
|
|
154
175
|
});
|
|
155
176
|
};
|
|
156
177
|
const me = Boolean(state?.yours);
|
|
157
|
-
return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.noBrowser }), error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state ? (_jsxs(Box, { mt: 2, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
|
|
178
|
+
return (_jsxs(Box, { "data-testid": "app-computer", children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: COMPUTER_WORDS.title }), _jsx(Parts, { app: app }), !used ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.none })) : (_jsxs(_Fragment, { children: [_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', m: 0, mt: 1 }, children: COMPUTER_WORDS.noBrowser }), noRuntime ? (_jsx(Text, { as: "p", role: "status", sx: { fontSize: 1, color: 'fg.default', m: 0, mt: 2 }, children: noRuntime })) : null, error ? (_jsx(Text, { as: "p", sx: { fontSize: 1, color: 'danger.fg', m: 0, mt: 2 }, children: error })) : null, state && context ? (_jsxs(Box, { mt: 2, "data-testid": "computer-holder", children: [_jsx(Text, { as: "p", sx: { fontSize: 1, m: 0, mb: 1 }, children: !state.held
|
|
158
179
|
? COMPUTER_WORDS.agentHas
|
|
159
180
|
: me
|
|
160
181
|
? COMPUTER_WORDS.youHave
|
|
161
|
-
: COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, _jsx(Terminal, { conversation: conversation }), state ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
|
|
182
|
+
: COMPUTER_WORDS.someoneHas }), !state.held ? (_jsx(Button, { size: "small", disabled: busy, onClick: () => act(takeOverComputer), children: COMPUTER_WORDS.takeOver })) : me ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "small", variant: "primary", disabled: busy, onClick: () => act(handBackComputer), children: COMPUTER_WORDS.handBack }), _jsx(YourTerminal, { context: context })] })) : null] })) : null, _jsx(Terminal, { conversation: conversation }), state && context ? (_jsx(Files, { context: context, state: state, ended: ended })) : null] }))] }));
|
|
162
183
|
}
|
|
163
184
|
export default AppComputer;
|
|
@@ -5,7 +5,8 @@ import { Box } from '@datalayer/primer-addons';
|
|
|
5
5
|
import { signal } from '@datalayer/reactor';
|
|
6
6
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
7
7
|
import { catalogOfBlocks } from '../../../components/a2ui';
|
|
8
|
-
import {
|
|
8
|
+
import { useChatWords } from '../../../chat/ChatLanguage';
|
|
9
|
+
import { LoopCanvasBlock, LoopChatTurn, agentServerOf, noRuntimeSaid, } from '../../core';
|
|
9
10
|
import { InlineSurface, SURFACE_CATALOG_ID, } from '../a2ui-surface/InlineSurface';
|
|
10
11
|
import { useOwnComponents } from '../a2ui-surface/ownComponents';
|
|
11
12
|
import { appPageAction, appPageData, appPageMessages, pageInputsOf, pageOf, } from './appPageModel';
|
|
@@ -73,12 +74,25 @@ export function AppPage({ app, workspace }) {
|
|
|
73
74
|
...outputsData(outputs),
|
|
74
75
|
}), [app, turn, conversation, outputs]);
|
|
75
76
|
const token = useIAMStore(state => state.token) ?? '';
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
const chatText = useChatWords();
|
|
78
|
+
/*
|
|
79
|
+
* Where its page runs: the runtime its agent is on, or nothing while none
|
|
80
|
+
* is assigned — then a run is not sent and the page says why, rather than
|
|
81
|
+
* reaching for the host's server (on a hosted page, the page's own origin).
|
|
82
|
+
*/
|
|
83
|
+
const pageServer = agentServerOf(workspace.sandbox, workspace.serverUrl);
|
|
84
|
+
const pageContext = useRef(null);
|
|
85
|
+
pageContext.current =
|
|
86
|
+
pageServer === undefined
|
|
87
|
+
? null
|
|
88
|
+
: {
|
|
89
|
+
serverUrl: pageServer,
|
|
90
|
+
agentId: workspace.agentId || app.id,
|
|
91
|
+
token: token || undefined,
|
|
92
|
+
};
|
|
93
|
+
const noRuntime = useRef('');
|
|
94
|
+
noRuntime.current =
|
|
95
|
+
pageServer === undefined ? noRuntimeSaid(workspace.sandbox, chatText) : '';
|
|
82
96
|
const pageSession = useRef(null);
|
|
83
97
|
const pageRunning = useRef(false);
|
|
84
98
|
const pageNext = useRef(null);
|
|
@@ -96,6 +110,10 @@ export function AppPage({ app, workspace }) {
|
|
|
96
110
|
const next = pageNext.current;
|
|
97
111
|
pageNext.current = null;
|
|
98
112
|
const context = pageContext.current;
|
|
113
|
+
if (!context) {
|
|
114
|
+
setRefusal(noRuntime.current);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
99
117
|
try {
|
|
100
118
|
pageSession.current ??= openPageSession(context);
|
|
101
119
|
const turn = await runPageIn(context, await pageSession.current, next);
|
|
@@ -41,7 +41,7 @@ import { AGENTS_PLUGIN_NAME } from '../agents/plugin';
|
|
|
41
41
|
import { useConfig } from '../../../hooks/useConfig';
|
|
42
42
|
import { useSkills, useSkillActions } from '../../../hooks/useSkills';
|
|
43
43
|
import { AI_MODEL_CATALOGUE, isChatModel } from '../../../specs/models';
|
|
44
|
-
import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopCommand, LoopFrontendTool, LoopRunProps, LoopSlots, canOpenView, runForwardedProps, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
|
|
44
|
+
import { LoopAgentBlueprint, LoopAgentGate, LoopChatComposer, LoopChatExtras, LoopChatHeader, LoopChatLayout, LoopChatSuggestion, LoopChatSurface, LoopChatTurn, LoopCommand, LoopFrontendTool, LoopRunProps, LoopSlots, agentServerOf, noRuntimeSaid, canOpenView, runForwardedProps, onPromptFocusRequest, onSurfaceRequest, useLoopPromptStore, } from '../../core';
|
|
45
45
|
import { messageText, turnWritersOf } from './turnState';
|
|
46
46
|
import { isInactiveSurfaceContribution, orderToolContributions, toolsForChatView, } from './chatViewTools';
|
|
47
47
|
import { useAgentCommandTools } from '../../../tools/adapters/commands';
|
|
@@ -946,8 +946,12 @@ export default function ChatView({ workspace }) {
|
|
|
946
946
|
* opened against. Addressing the latter would be talking to a machine that
|
|
947
947
|
* has never heard of it. The same value for every other target, where the
|
|
948
948
|
* two are the same server.
|
|
949
|
+
*
|
|
950
|
+
* Nothing while a Datalayer runtime is not assigned (the pool had none, or
|
|
951
|
+
* it is still being given): the chat is off and says so below, rather than
|
|
952
|
+
* sending to the host's server — on a hosted page, the page's own origin.
|
|
949
953
|
*/
|
|
950
|
-
const agentServerUrl = workspace.sandbox
|
|
954
|
+
const agentServerUrl = agentServerOf(workspace.sandbox, workspace.serverUrl);
|
|
951
955
|
/*
|
|
952
956
|
* Who a single request may be addressed to.
|
|
953
957
|
*
|
|
@@ -1041,12 +1045,25 @@ export default function ChatView({ workspace }) {
|
|
|
1041
1045
|
* Studio's Preview and the hosted page, rather than left unanswered.
|
|
1042
1046
|
*/
|
|
1043
1047
|
const ambient = useChatAvailability();
|
|
1044
|
-
|
|
1048
|
+
/*
|
|
1049
|
+
* No agent to talk to: a Datalayer runtime not assigned (STUDIO P-24,
|
|
1050
|
+
* round 9). Said in a sentence — why, when its launch was refused — and
|
|
1051
|
+
* nothing is sent; the header does not say *Ready*.
|
|
1052
|
+
*/
|
|
1053
|
+
const noRuntime = agentServerUrl === undefined
|
|
1054
|
+
? noRuntimeSaid(workspace.sandbox, chatText)
|
|
1055
|
+
: undefined;
|
|
1056
|
+
const chatDisabled = gateBlocked ||
|
|
1057
|
+
keyExpired ||
|
|
1058
|
+
Boolean(inPageRefusal) ||
|
|
1059
|
+
Boolean(noRuntime) ||
|
|
1060
|
+
ambient.disabled;
|
|
1045
1061
|
const disabledReason = keyExpired
|
|
1046
1062
|
? expiredKeyIsTemporary
|
|
1047
1063
|
? chatText.demoKeyExpired
|
|
1048
1064
|
: chatText.keyExpired
|
|
1049
1065
|
: (inPageRefusal ??
|
|
1066
|
+
noRuntime ??
|
|
1050
1067
|
gateReason ??
|
|
1051
1068
|
(ambient.disabled ? ambient.disableReason : undefined));
|
|
1052
1069
|
/*
|
|
@@ -1250,6 +1267,10 @@ export default function ChatView({ workspace }) {
|
|
|
1250
1267
|
? openedThread.thread
|
|
1251
1268
|
: undefined
|
|
1252
1269
|
: givenThread;
|
|
1270
|
+
/*
|
|
1271
|
+
* None without a server (`noRuntime`): the chat is off and connects to
|
|
1272
|
+
* nothing — no address is made up for it.
|
|
1273
|
+
*/
|
|
1253
1274
|
const protocol = useMemo(() => inPage
|
|
1254
1275
|
? browserProtocolConfig({
|
|
1255
1276
|
agentId: member?.specId ?? agentId,
|
|
@@ -1280,23 +1301,25 @@ export default function ChatView({ workspace }) {
|
|
|
1280
1301
|
// its session API does on a runtime.
|
|
1281
1302
|
modeEffect: blueprintTurn?.modeEffect,
|
|
1282
1303
|
})
|
|
1283
|
-
:
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1304
|
+
: agentServerUrl !== undefined
|
|
1305
|
+
? {
|
|
1306
|
+
type: 'ag-ui',
|
|
1307
|
+
// An application's agent is spoken to through its session API
|
|
1308
|
+
// (LOOP R-04): each thread a session, recorded under its uid,
|
|
1309
|
+
// run in the name the application runs in, and taking what the
|
|
1310
|
+
// page did besides the message — the same AG-UI events back.
|
|
1311
|
+
endpoint: runsApp
|
|
1312
|
+
? `${agentServerUrl}/api/v1/apps/agents/${encodeURIComponent(agentId)}/ag-ui/`
|
|
1313
|
+
: `${agentServerUrl}/api/v1/ag-ui/${agentId}/`,
|
|
1314
|
+
...(runsApp && iamToken ? { authToken: iamToken } : {}),
|
|
1315
|
+
agentId,
|
|
1316
|
+
// `/api/v1/configure`, not `/api/v1/configure/config`: the hooks
|
|
1317
|
+
// strip one trailing `config`/`configure` segment to find the API
|
|
1318
|
+
// base, so the longer form doubles it.
|
|
1319
|
+
configEndpoint: `${agentServerUrl}/api/v1/configure`,
|
|
1320
|
+
enableConfigQuery: true,
|
|
1321
|
+
}
|
|
1322
|
+
: undefined, [
|
|
1300
1323
|
agentId,
|
|
1301
1324
|
inPage,
|
|
1302
1325
|
inference,
|
|
@@ -1321,7 +1344,7 @@ export default function ChatView({ workspace }) {
|
|
|
1321
1344
|
* need one, and a browser agent has none — hence the menus below being
|
|
1322
1345
|
* gated on the data rather than switched on unconditionally.
|
|
1323
1346
|
*/
|
|
1324
|
-
const configQuery = useConfig(!inPage, protocol
|
|
1347
|
+
const configQuery = useConfig(!inPage, protocol?.type === 'ag-ui' ? protocol.configEndpoint : undefined, undefined, agentId);
|
|
1325
1348
|
const skillsQuery = useSkills(true);
|
|
1326
1349
|
const skillActions = useSkillActions(agentId);
|
|
1327
1350
|
// `undefined` until the runtime answers: its answer is then the list.
|
|
@@ -1338,7 +1361,7 @@ export default function ChatView({ workspace }) {
|
|
|
1338
1361
|
* catalogue that was never going to arrive. The agentspecs catalogue below
|
|
1339
1362
|
* is the answer for this case and needs no request at all.
|
|
1340
1363
|
*/
|
|
1341
|
-
if (inPage) {
|
|
1364
|
+
if (inPage || agentServerUrl === undefined) {
|
|
1342
1365
|
return undefined;
|
|
1343
1366
|
}
|
|
1344
1367
|
let cancelled = false;
|
|
@@ -1424,7 +1447,7 @@ export default function ChatView({ workspace }) {
|
|
|
1424
1447
|
*/
|
|
1425
1448
|
const selectModel = useCallback(async (model) => {
|
|
1426
1449
|
setPickedModel(model);
|
|
1427
|
-
if (inPage) {
|
|
1450
|
+
if (inPage || agentServerUrl === undefined) {
|
|
1428
1451
|
return;
|
|
1429
1452
|
}
|
|
1430
1453
|
await fetch(`${agentServerUrl}/api/v1/configure/inference/provider`, {
|
|
@@ -1663,7 +1686,9 @@ export default function ChatView({ workspace }) {
|
|
|
1663
1686
|
brandIcon: presence?.face ? (_jsx(PresenceFace, { face: presence.face, size: 20, state: presenceNow })) : (_jsx(BrandIcon, { size: 20 })),
|
|
1664
1687
|
// An application says what it is doing, in plain words, beside
|
|
1665
1688
|
// its name (T-08); an agent's chat keeps its header as it is.
|
|
1666
|
-
headerContent:
|
|
1689
|
+
headerContent:
|
|
1690
|
+
// Not *Ready* with no runtime: the header says why instead.
|
|
1691
|
+
presence && !noRuntime ? (_jsx(PresenceLine, { state: presenceNow })) : undefined, emptyState: {
|
|
1667
1692
|
// An application's own face, at the page's size of the three
|
|
1668
1693
|
// (LOOP T-08, T-19): the thing a person's eye lands on first.
|
|
1669
1694
|
icon: presence?.face ? (_jsx("span", { "aria-hidden": true, style: { fontSize: FACE_LARGE, lineHeight: 1 }, children: _jsx(FaceDrawing, { face: presence.face, size: FACE_LARGE }) })) : (_jsx(BrandIcon, { size: 48 })),
|