@datalayer/agent-runtimes 1.3.51 → 1.3.53
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/ChatFloating.d.ts +1 -1
- package/lib/chat/ChatFloating.js +33 -6
- package/lib/chat/assistant/AssistantStage.d.ts +4 -2
- package/lib/chat/assistant/AssistantStage.js +16 -1
- package/lib/chat/assistant/SpeechBalloon.d.ts +6 -2
- package/lib/chat/assistant/SpeechBalloon.js +4 -3
- package/lib/chat/assistant/formats/stateAnimations.js +3 -0
- package/lib/chat/assistant/state.d.ts +37 -5
- package/lib/chat/assistant/state.js +18 -3
- package/lib/chat/presence/Presence.d.ts +2 -1
- package/lib/chat/presence/Presence.js +11 -3
- package/lib/chat/presence/presenceStatus.d.ts +10 -4
- package/lib/chat/presence/presenceStatus.js +9 -2
- package/lib/loop/apps/AppRenderer.d.ts +6 -1
- package/lib/loop/apps/AppRenderer.js +2 -1
- package/lib/loop/core/index.d.ts +8 -0
- package/lib/loop/embed/LoopEmbed.js +1 -0
- package/lib/loop/plugins/assistant/index.d.ts +19 -9
- package/lib/loop/plugins/assistant/index.js +67 -25
- package/lib/loop/plugins/chat/ChatView.js +4 -2
- package/lib/loop/plugins/chat/index.d.ts +6 -1
- package/lib/loop/plugins/chat/index.js +1 -0
- package/lib/loop/plugins/chat/turnState.d.ts +5 -0
- package/lib/loop/plugins/chat/turnState.js +7 -0
- package/lib/stories/loop/ReferenceScreens.d.ts +1 -1
- package/lib/stories/loop/ReferenceScreens.js +16 -2
- package/package.json +1 -1
|
@@ -31,7 +31,7 @@ import type { ChatCommonProps, ChatViewMode, ThemeOverrides } from '../types';
|
|
|
31
31
|
export type FloatingConversation = {
|
|
32
32
|
/** What the window holds, under its header (the face, the title, close). */
|
|
33
33
|
body: React.ReactNode;
|
|
34
|
-
/** What it is doing: idle, thinking, working, waiting for the person. */
|
|
34
|
+
/** What it is doing: idle, thinking, working, waiting for the person, paused. */
|
|
35
35
|
presence: PresenceState;
|
|
36
36
|
/** Its newest words, said in the balloon until heard. */
|
|
37
37
|
saying?: AssistantSaying;
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -33,7 +33,7 @@ import { disabledChatViewModes, resolveMountPoint } from './viewModes';
|
|
|
33
33
|
import { AssistantStage } from './assistant/AssistantStage';
|
|
34
34
|
import { SpeechBalloon } from './assistant/SpeechBalloon';
|
|
35
35
|
import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
|
|
36
|
-
import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, } from './assistant/state';
|
|
36
|
+
import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, ASSISTANT_WORDS, } from './assistant/state';
|
|
37
37
|
import { presenceState, presenceToolOf, } from './presence/presenceStatus';
|
|
38
38
|
import { useChatOpen, useChatMessages, useChatStore, } from '../stores/chatStore';
|
|
39
39
|
import { useChatKeyboardShortcuts, getShortcutDisplay, } from '@datalayer/core/lib/hooks';
|
|
@@ -172,13 +172,40 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
172
172
|
speaking: answering,
|
|
173
173
|
});
|
|
174
174
|
const unheard = !!saying && saying.id !== heardId;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
/*
|
|
176
|
+
* The approval it waits on, answered in the balloon (T-23) by the chat's
|
|
177
|
+
* own answers to its approvals — those its tool-approval banner sends.
|
|
178
|
+
*/
|
|
179
|
+
const [deciding, setDeciding] = useState(false);
|
|
180
|
+
const firstPending = pendingApprovals?.[0];
|
|
181
|
+
const balloonApproval = firstPending && onApproveApproval && onRejectApproval
|
|
182
|
+
? {
|
|
183
|
+
id: firstPending.id,
|
|
184
|
+
asks: firstPending.toolDescription || firstPending.toolName,
|
|
185
|
+
others: (pendingApprovals?.length ?? 1) - 1,
|
|
186
|
+
deciding,
|
|
187
|
+
onApprove: () => {
|
|
188
|
+
setDeciding(true);
|
|
189
|
+
void Promise.resolve(onApproveApproval(firstPending.id)).finally(() => setDeciding(false));
|
|
190
|
+
},
|
|
191
|
+
onDeny: () => {
|
|
192
|
+
setDeciding(true);
|
|
193
|
+
void Promise.resolve(onRejectApproval(firstPending.id)).finally(() => setDeciding(false));
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
: undefined;
|
|
197
|
+
const assistantBalloon = assistantState === 'paused'
|
|
198
|
+
? { text: ASSISTANT_WORDS.paused }
|
|
199
|
+
: balloonApproval
|
|
200
|
+
? { text: ASSISTANT_WORDS.approval, approval: balloonApproval }
|
|
201
|
+
: assistantState === 'waiting'
|
|
202
|
+
? { text: ASSISTANT_WORDS.waiting }
|
|
203
|
+
: unheard && saying
|
|
204
|
+
? { text: saying.text, more: saying.more }
|
|
205
|
+
: { text: description };
|
|
180
206
|
const balloonInsists = assistantState === 'greeting' ||
|
|
181
207
|
assistantState === 'waiting' ||
|
|
208
|
+
!!balloonApproval ||
|
|
182
209
|
(unheard && (assistantState === 'speaking' || freshSaying));
|
|
183
210
|
const panelOnLoadingChange = panelProps?.onLoadingChange;
|
|
184
211
|
const panelOnDisplayItemsChange = panelProps?.onDisplayItemsChange;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import type { JSX, RefObject } from 'react';
|
|
18
18
|
import { type AssistantCharacter } from './characters';
|
|
19
19
|
import type { AssistantCharacterData } from './formats/types';
|
|
20
|
-
import { type AssistantAway, type AssistantState } from './state';
|
|
20
|
+
import { type AssistantAway, type AssistantState, type BalloonApproval } from './state';
|
|
21
21
|
/**
|
|
22
22
|
* Where the balloon goes so that it stays inside the window (T-23): toward
|
|
23
23
|
* the middle of the page from whichever half the character stands in, and
|
|
@@ -69,11 +69,13 @@ export interface AssistantStageProps {
|
|
|
69
69
|
/**
|
|
70
70
|
* What the balloon says while the conversation is closed: the agent's
|
|
71
71
|
* newest words, as the Office Assistant said them (T-23), or a welcome.
|
|
72
|
-
* `more` adds *Open the conversation* for the rest
|
|
72
|
+
* `more` adds *Open the conversation* for the rest; `approval`, an
|
|
73
|
+
* approval to answer there, with *Approve* and *Deny*.
|
|
73
74
|
*/
|
|
74
75
|
balloon?: {
|
|
75
76
|
text: string;
|
|
76
77
|
more?: boolean;
|
|
78
|
+
approval?: BalloonApproval;
|
|
77
79
|
};
|
|
78
80
|
/** Show the balloon without being hovered: something new to say. */
|
|
79
81
|
insist?: boolean;
|
|
@@ -41,6 +41,10 @@ const MOTIONS = {
|
|
|
41
41
|
'0%, 100%': { transform: 'scaleY(1)' },
|
|
42
42
|
'50%': { transform: 'scaleY(2.2)' },
|
|
43
43
|
},
|
|
44
|
+
'@keyframes assistantDoze': {
|
|
45
|
+
'0%, 100%': { transform: 'translateY(2px) scale(1, 0.97)' },
|
|
46
|
+
'50%': { transform: 'translateY(3px) scale(1.02, 0.95)' },
|
|
47
|
+
},
|
|
44
48
|
'@keyframes assistantLeave': {
|
|
45
49
|
from: { transform: 'scale(1)', opacity: 1 },
|
|
46
50
|
to: { transform: 'scale(0.2) translateY(20px)', opacity: 0 },
|
|
@@ -255,6 +259,17 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
255
259
|
'&[data-assistant-state="speaking"] .assistant-body': {
|
|
256
260
|
animation: 'assistantFloat 1.6s ease-in-out infinite',
|
|
257
261
|
},
|
|
262
|
+
// Paused (R-17): it dozes — its eyes shut, its breath slow, a little
|
|
263
|
+
// greyed — and, still, it is asleep: the shut eyes are not a motion.
|
|
264
|
+
'&[data-assistant-state="paused"] .assistant-body': {
|
|
265
|
+
animation: 'assistantDoze 4.8s ease-in-out infinite',
|
|
266
|
+
filter: 'grayscale(0.5)',
|
|
267
|
+
opacity: 0.8,
|
|
268
|
+
},
|
|
269
|
+
'&[data-assistant-state="paused"] .assistant-lids ellipse': {
|
|
270
|
+
animation: 'none',
|
|
271
|
+
transform: 'scaleY(1)',
|
|
272
|
+
},
|
|
258
273
|
'&[data-assistant-state="goodbye"] .assistant-body': {
|
|
259
274
|
animation: 'assistantLeave 0.6s ease-in forwards',
|
|
260
275
|
},
|
|
@@ -262,7 +277,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
262
277
|
'@media (prefers-reduced-motion: reduce)': {
|
|
263
278
|
'& *': { animation: 'none !important' },
|
|
264
279
|
},
|
|
265
|
-
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
|
|
280
|
+
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
|
|
266
281
|
pressedAt.current = { x: event.clientX, y: event.clientY };
|
|
267
282
|
onDragStart(event);
|
|
268
283
|
}, onClick: (event) => {
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* A speech balloon over something that stands for the agent — the floating
|
|
3
3
|
* assistant's character, or the floating popup's button while the chat is
|
|
4
4
|
* closed: the agent's newest words, as the Office Assistant said them
|
|
5
|
-
* (LOOP T-23), with *Open the conversation* when there is more
|
|
5
|
+
* (LOOP T-23), with *Open the conversation* when there is more — and an
|
|
6
|
+
* approval it waits on, answered there with *Approve* or *Deny*.
|
|
6
7
|
*
|
|
7
8
|
* @module chat/assistant/SpeechBalloon
|
|
8
9
|
*/
|
|
9
10
|
import type { JSX } from 'react';
|
|
11
|
+
import { type BalloonApproval } from './state';
|
|
10
12
|
export interface SpeechBalloonProps {
|
|
11
13
|
text: string;
|
|
12
14
|
/** There is more than the balloon holds. */
|
|
@@ -21,6 +23,8 @@ export interface SpeechBalloonProps {
|
|
|
21
23
|
align: 'left' | 'right';
|
|
22
24
|
/** Where the tail points, from that edge, in pixels. */
|
|
23
25
|
tailAt: number;
|
|
26
|
+
/** An approval it waits on: what is asked, and the two answers. */
|
|
27
|
+
approval?: BalloonApproval;
|
|
24
28
|
}
|
|
25
|
-
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, }: SpeechBalloonProps): JSX.Element;
|
|
29
|
+
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, }: SpeechBalloonProps): JSX.Element;
|
|
26
30
|
export default SpeechBalloon;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Text } from '@primer/react';
|
|
2
|
+
import { Button, Text } from '@primer/react';
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
|
-
|
|
4
|
+
import { ASSISTANT_WORDS } from './state';
|
|
5
|
+
export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, }) {
|
|
5
6
|
return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
|
|
6
7
|
position: 'absolute',
|
|
7
8
|
// Pixels, as strings: a number here is read as the theme's space
|
|
@@ -36,7 +37,7 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
36
37
|
borderColor: 'border.default',
|
|
37
38
|
transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
|
|
38
39
|
},
|
|
39
|
-
}, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
|
|
40
|
+
}, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: `${approval.others} more waiting.` })) : null] })), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
|
|
40
41
|
mt: 1,
|
|
41
42
|
p: 0,
|
|
42
43
|
border: 0,
|
|
@@ -33,6 +33,7 @@ const NAMES = {
|
|
|
33
33
|
'GestureDown',
|
|
34
34
|
'Acknowledge',
|
|
35
35
|
],
|
|
36
|
+
paused: ['IdleSnooze', 'Sleep', 'Sleeping', 'Snooze', 'Rest', 'RestPose'],
|
|
36
37
|
greeting: ['Greeting', 'Greet', 'Show', 'Wave'],
|
|
37
38
|
speaking: [
|
|
38
39
|
'Explain',
|
|
@@ -50,6 +51,7 @@ const AUTHORED = {
|
|
|
50
51
|
thinking: [],
|
|
51
52
|
working: [],
|
|
52
53
|
waiting: ['Listening'],
|
|
54
|
+
paused: [],
|
|
53
55
|
greeting: ['Showing'],
|
|
54
56
|
speaking: ['Speaking'],
|
|
55
57
|
goodbye: ['Hiding'],
|
|
@@ -60,6 +62,7 @@ const STATES = [
|
|
|
60
62
|
'thinking',
|
|
61
63
|
'working',
|
|
62
64
|
'waiting',
|
|
65
|
+
'paused',
|
|
63
66
|
'greeting',
|
|
64
67
|
'speaking',
|
|
65
68
|
'goodbye',
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What the floating assistant acts out (LOOP T-22): the presence of the
|
|
3
|
-
* application (T-08)
|
|
3
|
+
* application (T-08) — paused among them, when its owner paused it — and
|
|
4
|
+
* three moments of its own — greeting when it
|
|
4
5
|
* arrives, speaking while an answer streams in, goodbye when it is sent away.
|
|
5
6
|
*
|
|
6
7
|
* @module chat/assistant/state
|
|
7
8
|
*/
|
|
8
9
|
import type { PresenceState } from '../presence/presenceStatus';
|
|
9
10
|
/** The states a character has an animation for. */
|
|
10
|
-
export type AssistantState = 'idle' | 'thinking' | 'working' | 'waiting' | 'greeting' | 'speaking' | 'goodbye';
|
|
11
|
+
export type AssistantState = 'idle' | 'thinking' | 'working' | 'waiting' | 'paused' | 'greeting' | 'speaking' | 'goodbye';
|
|
11
12
|
/** What a character is doing, beyond what the application is doing. */
|
|
12
13
|
export interface AssistantMoment {
|
|
13
14
|
/** It has just appeared, and says hello once. */
|
|
@@ -20,9 +21,10 @@ export interface AssistantMoment {
|
|
|
20
21
|
/**
|
|
21
22
|
* The state to act, from the application's presence and the moment.
|
|
22
23
|
*
|
|
23
|
-
* Leaving
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Leaving wins, being short; then paused — a paused application neither
|
|
25
|
+
* greets nor speaks, it dozes; then arriving; then waiting for the person,
|
|
26
|
+
* which must never be missed; then speaking over thinking, since words
|
|
27
|
+
* arriving are what the person watches; then the presence as it is.
|
|
26
28
|
*/
|
|
27
29
|
export declare function assistantStateOf(presence: PresenceState, moment?: AssistantMoment): AssistantState;
|
|
28
30
|
/**
|
|
@@ -30,6 +32,36 @@ export declare function assistantStateOf(presence: PresenceState, moment?: Assis
|
|
|
30
32
|
* assistant's own message, not a tool call. Read with `busy`, it is speaking.
|
|
31
33
|
*/
|
|
32
34
|
export declare function newestIsAnswer(items: readonly unknown[]): boolean;
|
|
35
|
+
/** What the balloon says for a state, when the agent has said nothing newer. */
|
|
36
|
+
export declare const ASSISTANT_WORDS: {
|
|
37
|
+
/** An approval waits and cannot be answered in the balloon itself. */
|
|
38
|
+
readonly waiting: "Waiting for you — open the conversation to answer.";
|
|
39
|
+
/** It waits on the person's yes: Approve or Deny, in the balloon (T-23). */
|
|
40
|
+
readonly approval: "Waiting for you";
|
|
41
|
+
readonly approve: "Approve";
|
|
42
|
+
readonly deny: "Deny";
|
|
43
|
+
/** Its deployment is paused (R-17). */
|
|
44
|
+
readonly paused: "Paused — it answers again once it is resumed.";
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* An approval asked of the person, answered in the balloon (T-23): what is
|
|
48
|
+
* asked, under which rule, and the two answers, sent where every approval is
|
|
49
|
+
* answered (U-19).
|
|
50
|
+
*/
|
|
51
|
+
export interface BalloonApproval {
|
|
52
|
+
/** Its id: a new one is a new question. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** What it asks to do: the tool it would call. */
|
|
55
|
+
asks: string;
|
|
56
|
+
/** The rule or check it was asked under, when the runtime said. */
|
|
57
|
+
why?: string;
|
|
58
|
+
/** How many more wait after this one. */
|
|
59
|
+
others: number;
|
|
60
|
+
/** An answer is being sent. */
|
|
61
|
+
deciding?: boolean;
|
|
62
|
+
onApprove: () => void;
|
|
63
|
+
onDeny: () => void;
|
|
64
|
+
}
|
|
33
65
|
/** A word from the agent, for the character's balloon. */
|
|
34
66
|
export interface AssistantSaying {
|
|
35
67
|
/** The message it comes from: a new id is a new thing said. */
|
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* The state to act, from the application's presence and the moment.
|
|
7
7
|
*
|
|
8
|
-
* Leaving
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Leaving wins, being short; then paused — a paused application neither
|
|
9
|
+
* greets nor speaks, it dozes; then arriving; then waiting for the person,
|
|
10
|
+
* which must never be missed; then speaking over thinking, since words
|
|
11
|
+
* arriving are what the person watches; then the presence as it is.
|
|
11
12
|
*/
|
|
12
13
|
export function assistantStateOf(presence, moment = {}) {
|
|
13
14
|
if (moment.leaving) {
|
|
14
15
|
return 'goodbye';
|
|
15
16
|
}
|
|
17
|
+
if (presence === 'paused') {
|
|
18
|
+
return 'paused';
|
|
19
|
+
}
|
|
16
20
|
if (moment.arriving) {
|
|
17
21
|
return 'greeting';
|
|
18
22
|
}
|
|
@@ -34,6 +38,17 @@ export function newestIsAnswer(items) {
|
|
|
34
38
|
newest.role === 'assistant' &&
|
|
35
39
|
typeof newest.toolName !== 'string');
|
|
36
40
|
}
|
|
41
|
+
/** What the balloon says for a state, when the agent has said nothing newer. */
|
|
42
|
+
export const ASSISTANT_WORDS = {
|
|
43
|
+
/** An approval waits and cannot be answered in the balloon itself. */
|
|
44
|
+
waiting: 'Waiting for you — open the conversation to answer.',
|
|
45
|
+
/** It waits on the person's yes: Approve or Deny, in the balloon (T-23). */
|
|
46
|
+
approval: 'Waiting for you',
|
|
47
|
+
approve: 'Approve',
|
|
48
|
+
deny: 'Deny',
|
|
49
|
+
/** Its deployment is paused (R-17). */
|
|
50
|
+
paused: 'Paused — it answers again once it is resumed.',
|
|
51
|
+
};
|
|
37
52
|
/** How much of a message the balloon says before *Open the conversation*. */
|
|
38
53
|
export const SAYING_LIMIT = 220;
|
|
39
54
|
/**
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* motion around it while it thinks or works, and one line saying what it is
|
|
4
4
|
* doing. The face is never redrawn; the motion is a soft ring in the accent,
|
|
5
5
|
* still under `prefers-reduced-motion`, and the line changes at the theme's
|
|
6
|
-
* status pace (T-10).
|
|
6
|
+
* status pace (T-10). Paused, the ring is a still, muted dashed circle and
|
|
7
|
+
* the face is dimmed: nothing moves around something that does nothing.
|
|
7
8
|
*
|
|
8
9
|
* @module chat/presence/Presence
|
|
9
10
|
*/
|
|
@@ -13,6 +13,7 @@ export function FaceDrawing({ face, size }) {
|
|
|
13
13
|
}
|
|
14
14
|
export function PresenceFace({ face, size, state, }) {
|
|
15
15
|
const active = state === 'thinking' || state === 'working';
|
|
16
|
+
const paused = state === 'paused';
|
|
16
17
|
return (_jsxs(Box, { as: "span", "aria-hidden": true, "data-presence": state, sx: {
|
|
17
18
|
position: 'relative',
|
|
18
19
|
display: 'inline-flex',
|
|
@@ -27,8 +28,10 @@ export function PresenceFace({ face, size, state, }) {
|
|
|
27
28
|
position: 'absolute',
|
|
28
29
|
inset: 0,
|
|
29
30
|
borderRadius: '50%',
|
|
30
|
-
boxShadow: `0 0 0 2px ${ACCENT}`,
|
|
31
|
-
|
|
31
|
+
boxShadow: paused ? 'none' : `0 0 0 2px ${ACCENT}`,
|
|
32
|
+
border: paused ? '2px dashed' : 'none',
|
|
33
|
+
borderColor: 'border.default',
|
|
34
|
+
opacity: active || state === 'waiting' || paused ? 0.9 : 0,
|
|
32
35
|
transition: 'opacity var(--theme-motion-status, 0ms) var(--theme-motion-easing, ease)',
|
|
33
36
|
animation: active
|
|
34
37
|
? 'loopPresenceBreath 1.8s ease-in-out infinite'
|
|
@@ -40,7 +43,12 @@ export function PresenceFace({ face, size, state, }) {
|
|
|
40
43
|
'@media (prefers-reduced-motion: reduce)': {
|
|
41
44
|
animation: 'none',
|
|
42
45
|
},
|
|
43
|
-
} }), _jsx(
|
|
46
|
+
} }), _jsx(Box, { as: "span", sx: {
|
|
47
|
+
display: 'inline-flex',
|
|
48
|
+
opacity: paused ? 0.55 : 1,
|
|
49
|
+
filter: paused ? 'grayscale(0.6)' : 'none',
|
|
50
|
+
transition: 'opacity var(--theme-motion-status, 0ms) var(--theme-motion-easing, ease)',
|
|
51
|
+
}, children: _jsx(FaceDrawing, { face: face, size: size }) })] }));
|
|
44
52
|
}
|
|
45
53
|
export function PresenceLine({ state }) {
|
|
46
54
|
return (_jsx(Text, { role: "status", "aria-live": "polite", "data-presence-line": state, sx: {
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module chat/presence/presenceStatus
|
|
6
6
|
*/
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The states a person is told about. Paused is its deployment's: its owner
|
|
9
|
+
* paused it (LOOP R-17), and it answers nobody until it is resumed.
|
|
10
|
+
*/
|
|
11
|
+
export type PresenceState = 'idle' | 'thinking' | 'working' | 'waiting' | 'paused';
|
|
9
12
|
/** The line each state is said by. */
|
|
10
13
|
export declare const PRESENCE_LINES: Record<PresenceState, string>;
|
|
11
14
|
/** The newest tool call of the conversation, as far as presence reads it. */
|
|
@@ -16,14 +19,17 @@ export interface PresenceTool {
|
|
|
16
19
|
pendingApproval: boolean;
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
|
-
* The state, from whether a turn is running
|
|
22
|
+
* The state, from whether a turn is running, the newest tool call, and
|
|
23
|
+
* whether its deployment is paused.
|
|
20
24
|
*
|
|
25
|
+
* Paused wins: a paused application does nothing, whatever its last turn
|
|
26
|
+
* left behind.
|
|
21
27
|
* Waiting for an approval is said whether or not the turn is still
|
|
22
28
|
* streaming — the stream may end while the approval is asked. A tool that
|
|
23
29
|
* has not returned counts as work only during a turn: a stopped turn can
|
|
24
30
|
* leave one open, and the application is not working then.
|
|
25
31
|
*/
|
|
26
|
-
export declare function presenceState(busy: boolean, tool?: PresenceTool): PresenceState;
|
|
32
|
+
export declare function presenceState(busy: boolean, tool?: PresenceTool, paused?: boolean): PresenceState;
|
|
27
33
|
/**
|
|
28
34
|
* The newest tool call of the conversation: whether it has not returned, and
|
|
29
35
|
* whether it waits on a person's approval. A tool call is an item with a
|
|
@@ -8,16 +8,23 @@ export const PRESENCE_LINES = {
|
|
|
8
8
|
thinking: 'Thinking…',
|
|
9
9
|
working: 'Working…',
|
|
10
10
|
waiting: 'Waiting for you',
|
|
11
|
+
paused: 'Paused',
|
|
11
12
|
};
|
|
12
13
|
/**
|
|
13
|
-
* The state, from whether a turn is running
|
|
14
|
+
* The state, from whether a turn is running, the newest tool call, and
|
|
15
|
+
* whether its deployment is paused.
|
|
14
16
|
*
|
|
17
|
+
* Paused wins: a paused application does nothing, whatever its last turn
|
|
18
|
+
* left behind.
|
|
15
19
|
* Waiting for an approval is said whether or not the turn is still
|
|
16
20
|
* streaming — the stream may end while the approval is asked. A tool that
|
|
17
21
|
* has not returned counts as work only during a turn: a stopped turn can
|
|
18
22
|
* leave one open, and the application is not working then.
|
|
19
23
|
*/
|
|
20
|
-
export function presenceState(busy, tool) {
|
|
24
|
+
export function presenceState(busy, tool, paused = false) {
|
|
25
|
+
if (paused) {
|
|
26
|
+
return 'paused';
|
|
27
|
+
}
|
|
21
28
|
if (tool?.open && tool.pendingApproval) {
|
|
22
29
|
return 'waiting';
|
|
23
30
|
}
|
|
@@ -89,6 +89,11 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
|
|
|
89
89
|
* stable function, such as a state setter.
|
|
90
90
|
*/
|
|
91
91
|
onPresence?: (state: PresenceState) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Its deployment is paused (LOOP R-17), as its host knows it: its chat
|
|
94
|
+
* says *Paused* beside its face and its assistant dozes (T-08, T-22).
|
|
95
|
+
*/
|
|
96
|
+
paused?: boolean;
|
|
92
97
|
/**
|
|
93
98
|
* Told what the application last said, and whether its newest words are
|
|
94
99
|
* an answer — for a host that says them outside the conversation: the
|
|
@@ -195,5 +200,5 @@ export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
|
|
|
195
200
|
* what the page around it set does not reach in.
|
|
196
201
|
*/
|
|
197
202
|
export declare function appThemeOverrides(app: AppSpec): ThemeOverrides | undefined;
|
|
198
|
-
export declare function AppRenderer({ app, plugins, instance, onPresence, onSaying, sidebar, frame, page, pluginsOff, ...embed }: AppRendererProps): React.JSX.Element;
|
|
203
|
+
export declare function AppRenderer({ app, plugins, instance, onPresence, onSaying, paused, sidebar, frame, page, pluginsOff, ...embed }: AppRendererProps): React.JSX.Element;
|
|
199
204
|
export default AppRenderer;
|
|
@@ -252,7 +252,7 @@ export function appThemeOverrides(app) {
|
|
|
252
252
|
}
|
|
253
253
|
: undefined;
|
|
254
254
|
}
|
|
255
|
-
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, sidebar = false, frame = false, page, pluginsOff, ...embed }) {
|
|
255
|
+
export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, onSaying, paused = false, sidebar = false, frame = false, page, pluginsOff, ...embed }) {
|
|
256
256
|
/*
|
|
257
257
|
* The kind's plugins, made once per application. `LoopEmbed` rebuilds its
|
|
258
258
|
* whole reactor when its plugins change, so a new plugin on every render
|
|
@@ -325,6 +325,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
325
325
|
name: app.name,
|
|
326
326
|
face: app.emoji,
|
|
327
327
|
welcome: app.interface.welcome || app.description,
|
|
328
|
+
paused,
|
|
328
329
|
onPresence,
|
|
329
330
|
onSaying,
|
|
330
331
|
}, showTokenUsage: false, datalayerCreatePayload: datalayerCreatePayload, ...embed, plugins: allPlugins }));
|
package/lib/loop/core/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { FrontendToolDefinition } from '../../types/tools';
|
|
|
6
6
|
import type { ContextSnapshotData } from '../../types/context';
|
|
7
7
|
import type { AssistantCharacter } from '../../chat/assistant/characters';
|
|
8
8
|
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
9
|
+
import type { PresenceState } from '../../chat/presence/presenceStatus';
|
|
9
10
|
/** Lifecycle of the sandbox a workspace is attached to. */
|
|
10
11
|
export type SandboxState = 'idle' | 'starting' | 'running' | 'stopping' | 'error';
|
|
11
12
|
/** What the workspace knows about the sandbox behind it. */
|
|
@@ -983,5 +984,12 @@ export type ChatTurnContribution = {
|
|
|
983
984
|
* reply as it streams included; empty again when the chat starts over.
|
|
984
985
|
*/
|
|
985
986
|
conversation: ReadonlySignal<ConversationEntry[]>;
|
|
987
|
+
/**
|
|
988
|
+
* What the application is doing, as its chat says it beside its face
|
|
989
|
+
* (LOOP T-08) — waiting for the person on an approval, paused when its
|
|
990
|
+
* deployment is — for whoever acts it out away from the chat: the
|
|
991
|
+
* floating assistant (T-22).
|
|
992
|
+
*/
|
|
993
|
+
presence: ReadonlySignal<PresenceState>;
|
|
986
994
|
};
|
|
987
995
|
export declare const LoopChatTurn: import("@datalayer/reactor").ContributionPoint<ChatTurnContribution>;
|
|
@@ -72,6 +72,7 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
|
|
|
72
72
|
preset.presence?.name,
|
|
73
73
|
preset.presence?.face,
|
|
74
74
|
preset.presence?.welcome,
|
|
75
|
+
preset.presence?.paused,
|
|
75
76
|
preset.showTokenUsage,
|
|
76
77
|
// By its content: an application's spec written inline is a new object each render.
|
|
77
78
|
JSON.stringify(preset.datalayerCreatePayload ?? null),
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* page draws over its workspace, chosen from what the enabled plugins
|
|
4
4
|
* contribute to `loop.assistant.character` — the application's own
|
|
5
5
|
* (`interface.assistant`) when its Appspec names one, else the person's
|
|
6
|
-
* choice from their settings, else the paper clip. It acts out the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* choice from their settings, else the paper clip. It acts out what the
|
|
7
|
+
* application is doing as its chat says it — paused too, when its deployment
|
|
8
|
+
* is — and the turn (T-22), says the agent's newest words in its balloon
|
|
9
|
+
* while the conversation is out of sight, holds there the approval it waits
|
|
10
|
+
* on, answered with *Approve* or *Deny* on the path every approval of it is
|
|
11
|
+
* answered on (T-23, U-19), opens the conversation when clicked, and is
|
|
12
|
+
* dragged about and sent away as the chat's own assistant is (T-27).
|
|
10
13
|
*
|
|
11
14
|
* The workspace's chat is the conversation: on a page layout, its side panel
|
|
12
15
|
* is opened and closed; where the conversation is always on screen, a click
|
|
@@ -16,6 +19,7 @@
|
|
|
16
19
|
*/
|
|
17
20
|
import type { JSX } from 'react';
|
|
18
21
|
import { type AssistantState } from '../../../chat/assistant/state';
|
|
22
|
+
import type { PresenceState } from '../../../chat/presence/presenceStatus';
|
|
19
23
|
import { type ChatTurnSnapshot } from '../../core';
|
|
20
24
|
export declare const LOOP_ASSISTANT_PLUGIN_NAME = "@datalayer/loop-plugin-assistant";
|
|
21
25
|
export type LoopAssistantConfig = {
|
|
@@ -23,16 +27,22 @@ export type LoopAssistantConfig = {
|
|
|
23
27
|
app?: string;
|
|
24
28
|
/** The character the person chose in their settings. */
|
|
25
29
|
person?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The application's id: the approvals it waits on are read and answered
|
|
32
|
+
* in the balloon (T-23), as its rules card reads them (U-19). Without it,
|
|
33
|
+
* the balloon says an approval waits and the conversation answers it.
|
|
34
|
+
*/
|
|
35
|
+
appId?: string;
|
|
26
36
|
};
|
|
27
37
|
/**
|
|
28
|
-
* What the character acts out, from the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
38
|
+
* What the character acts out (T-22), from what the application is doing as
|
|
39
|
+
* its chat says it — idle, thinking, working, waiting for the person, paused
|
|
40
|
+
* — and the turn: it speaks while the answer is written.
|
|
31
41
|
*/
|
|
32
|
-
export declare function assistantStateOfTurn(turn: ChatTurnSnapshot, moment?: {
|
|
42
|
+
export declare function assistantStateOfTurn(turn: ChatTurnSnapshot, presence: PresenceState, moment?: {
|
|
33
43
|
arriving?: boolean;
|
|
34
44
|
leaving?: boolean;
|
|
35
45
|
}): AssistantState;
|
|
36
|
-
export declare function LoopAssistant({ app, person, }: LoopAssistantConfig): JSX.Element | null;
|
|
46
|
+
export declare function LoopAssistant({ app, person, appId, }: LoopAssistantConfig): JSX.Element | null;
|
|
37
47
|
export declare const LoopAssistantPlugin: import("@datalayer/reactor").ReactorPlugin<LoopAssistantConfig, unknown, unknown>;
|
|
38
48
|
export default LoopAssistantPlugin;
|
|
@@ -1,47 +1,80 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { definePlugin, signal } from '@datalayer/reactor';
|
|
4
4
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
5
5
|
import { Text } from '@primer/react';
|
|
6
6
|
import { Box } from '@datalayer/primer-addons';
|
|
7
|
+
import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
|
|
7
8
|
import { pageLayoutPanelOpen, togglePagePanel, } from '@datalayer/primer-addons/lib/reactor';
|
|
8
9
|
import { AssistantStage } from '../../../chat/assistant/AssistantStage';
|
|
9
|
-
import { assistantStateOf, keepAway, keptAway, latestSaying, } from '../../../chat/assistant/state';
|
|
10
|
+
import { ASSISTANT_WORDS, assistantStateOf, keepAway, keptAway, latestSaying, } from '../../../chat/assistant/state';
|
|
11
|
+
import { useApproveToolRequest, useRejectToolRequest, useToolApprovalsQuery, } from '../../../hooks/useToolApprovals';
|
|
12
|
+
import { approvalsOfApp, ruleOfApproval } from '../app-rules/AppRulesCard';
|
|
10
13
|
import { useViewportDrag } from '../../../chat/useViewportDrag';
|
|
11
14
|
import { LoopAssistantCharacter, LoopChatLayout, LoopChatTurn, LoopSlots, } from '../../core';
|
|
12
15
|
import { assistantCharacterFor } from '../assistant-characters';
|
|
13
16
|
export const LOOP_ASSISTANT_PLUGIN_NAME = '@datalayer/loop-plugin-assistant';
|
|
14
17
|
/** The character's size, in pixels, as the chat's own assistant. */
|
|
15
18
|
const SIZE = 88;
|
|
16
|
-
/*
|
|
19
|
+
/* Signals to read when no chat contributed a turn: the hooks need one. */
|
|
17
20
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
21
|
+
const NO_PRESENCE = signal('idle');
|
|
18
22
|
/**
|
|
19
|
-
* What the character acts out, from the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* What the character acts out (T-22), from what the application is doing as
|
|
24
|
+
* its chat says it — idle, thinking, working, waiting for the person, paused
|
|
25
|
+
* — and the turn: it speaks while the answer is written.
|
|
22
26
|
*/
|
|
23
|
-
export function assistantStateOfTurn(turn, moment = {}) {
|
|
24
|
-
const busy = turn.status === 'thinking' || turn.status === 'streaming';
|
|
25
|
-
const presence = !busy
|
|
26
|
-
? 'idle'
|
|
27
|
-
: turn.activity
|
|
28
|
-
? 'working'
|
|
29
|
-
: 'thinking';
|
|
27
|
+
export function assistantStateOfTurn(turn, presence, moment = {}) {
|
|
30
28
|
return assistantStateOf(presence, {
|
|
31
29
|
...moment,
|
|
32
30
|
speaking: turn.status === 'streaming' && !turn.activity,
|
|
33
31
|
});
|
|
34
32
|
}
|
|
33
|
+
const PENDING = { status: 'pending' };
|
|
34
|
+
/**
|
|
35
|
+
* The approvals an application waits on, read and answered as its rules
|
|
36
|
+
* card does (U-19): over the ai-agents `/ws`, where every decision is
|
|
37
|
+
* broadcast, so one answered here leaves every list. The first is offered
|
|
38
|
+
* to the balloon; none, once all are answered.
|
|
39
|
+
*/
|
|
40
|
+
function WaitingApprovals({ appId, onWaiting, }) {
|
|
41
|
+
const query = useToolApprovalsQuery(PENDING);
|
|
42
|
+
const approve = useApproveToolRequest();
|
|
43
|
+
const reject = useRejectToolRequest();
|
|
44
|
+
const waiting = useMemo(() => approvalsOfApp(query.data?.approvals ?? [], appId), [query.data, appId]);
|
|
45
|
+
const first = waiting[0];
|
|
46
|
+
const deciding = approve.isPending || reject.isPending;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
onWaiting(first
|
|
49
|
+
? {
|
|
50
|
+
id: first.id,
|
|
51
|
+
asks: first.tool_name,
|
|
52
|
+
why: ruleOfApproval(first) || undefined,
|
|
53
|
+
others: waiting.length - 1,
|
|
54
|
+
deciding,
|
|
55
|
+
onApprove: () => approve.mutate({ id: first.id }),
|
|
56
|
+
onDeny: () => reject.mutate({ id: first.id }),
|
|
57
|
+
}
|
|
58
|
+
: undefined);
|
|
59
|
+
// The mutations are new objects each render; the decision they send is not.
|
|
60
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
|
+
}, [first?.id, waiting.length, deciding, onWaiting]);
|
|
62
|
+
useEffect(() => () => onWaiting(undefined), [onWaiting]);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
35
65
|
/** Take the person to the workspace's composer. */
|
|
36
66
|
function focusComposer() {
|
|
37
67
|
const composer = document.querySelector('[data-chat-composer] textarea, [data-chat-composer] [contenteditable="true"]');
|
|
38
68
|
composer?.focus();
|
|
39
69
|
}
|
|
40
|
-
export function LoopAssistant({ app, person, }) {
|
|
70
|
+
export function LoopAssistant({ app, person, appId, }) {
|
|
41
71
|
const contributed = useContributions(LoopAssistantCharacter).map(entry => entry.value);
|
|
42
72
|
const chosen = assistantCharacterFor(contributed, { app, person });
|
|
43
73
|
const turnEntries = useContributions(LoopChatTurn);
|
|
44
74
|
const turn = useSignalValue(turnEntries[0]?.value.turn ?? NO_TURN);
|
|
75
|
+
const presence = useSignalValue(turnEntries[0]?.value.presence ?? NO_PRESENCE);
|
|
76
|
+
const signedIn = Boolean(useIAMStore(state => state.token));
|
|
77
|
+
const [approval, setApproval] = useState();
|
|
45
78
|
// On a page layout the conversation is its side panel; elsewhere it is
|
|
46
79
|
// always on screen.
|
|
47
80
|
const onPageLayout = useContributions(LoopChatLayout).some(entry => entry.value.id === 'page-layout');
|
|
@@ -96,12 +129,21 @@ export function LoopAssistant({ app, person, }) {
|
|
|
96
129
|
boxShadow: 'shadow.medium',
|
|
97
130
|
}, children: _jsx(Text, { sx: { fontSize: 1 }, children: chosen.problem }) }));
|
|
98
131
|
}
|
|
99
|
-
const state = assistantStateOfTurn(turn, { arriving, leaving });
|
|
132
|
+
const state = assistantStateOfTurn(turn, presence, { arriving, leaving });
|
|
100
133
|
const unheard = !!saying && saying.id !== heardId;
|
|
101
|
-
const balloon =
|
|
102
|
-
? { text:
|
|
103
|
-
:
|
|
104
|
-
|
|
134
|
+
const balloon = state === 'paused'
|
|
135
|
+
? { text: ASSISTANT_WORDS.paused }
|
|
136
|
+
: approval
|
|
137
|
+
? { text: ASSISTANT_WORDS.approval, approval }
|
|
138
|
+
: presence === 'waiting'
|
|
139
|
+
? { text: ASSISTANT_WORDS.waiting }
|
|
140
|
+
: unheard && saying
|
|
141
|
+
? { text: saying.text, more: saying.more }
|
|
142
|
+
: { text: 'Click me to open the conversation.' };
|
|
143
|
+
const insist = state === 'greeting' ||
|
|
144
|
+
presence === 'waiting' ||
|
|
145
|
+
(!!approval && state !== 'paused') ||
|
|
146
|
+
(unheard && (state === 'speaking' || fresh));
|
|
105
147
|
const onToggle = () => {
|
|
106
148
|
if (onPageLayout) {
|
|
107
149
|
togglePagePanel();
|
|
@@ -118,18 +160,18 @@ export function LoopAssistant({ app, person, }) {
|
|
|
118
160
|
setAway(next);
|
|
119
161
|
}, 600);
|
|
120
162
|
};
|
|
121
|
-
return (_jsx(AssistantStage, { character: chosen.character, state: state, size: SIZE, place: drag.position
|
|
122
|
-
|
|
123
|
-
|
|
163
|
+
return (_jsxs(_Fragment, { children: [appId && signedIn ? (_jsx(WaitingApprovals, { appId: appId, onWaiting: setApproval })) : null, _jsx(AssistantStage, { character: chosen.character, state: state, size: SIZE, place: drag.position
|
|
164
|
+
? { left: `${drag.position.left}px`, top: `${drag.position.top}px` }
|
|
165
|
+
: { right: '24px', bottom: '24px' }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: open, onToggle: onToggle, balloon: balloon, insist: insist, onDismiss: onDismiss })] }));
|
|
124
166
|
}
|
|
125
167
|
export const LoopAssistantPlugin = definePlugin({
|
|
126
168
|
name: LOOP_ASSISTANT_PLUGIN_NAME,
|
|
127
|
-
config: { app: undefined, person: undefined },
|
|
169
|
+
config: { app: undefined, person: undefined, appId: undefined },
|
|
128
170
|
displayName: 'Floating assistant',
|
|
129
171
|
description: 'The chat as a character on the page: the application’s own, or the one the person chose, from the characters the enabled plugins contribute.',
|
|
130
172
|
octicon: 'paperclip',
|
|
131
173
|
build: ({ config }) => {
|
|
132
|
-
const Configured = () => (_jsx(LoopAssistant, { app: config.app, person: config.person }));
|
|
174
|
+
const Configured = () => (_jsx(LoopAssistant, { app: config.app, person: config.person, appId: config.appId }));
|
|
133
175
|
return {
|
|
134
176
|
components: [
|
|
135
177
|
{ id: 'loop-assistant', slot: LoopSlots.root, Component: Configured },
|
|
@@ -199,11 +199,13 @@ export default function ChatView({ workspace }) {
|
|
|
199
199
|
open: false,
|
|
200
200
|
pendingApproval: false,
|
|
201
201
|
});
|
|
202
|
-
const presenceNow = presenceState(busy, presenceTool);
|
|
202
|
+
const presenceNow = presenceState(busy, presenceTool, config?.presence?.paused === true);
|
|
203
203
|
const onPresence = config?.presence?.onPresence;
|
|
204
|
-
// A host drawing the face in a frame of its own is told as it changes
|
|
204
|
+
// A host drawing the face in a frame of its own is told as it changes, and
|
|
205
|
+
// so is whoever reads the turn: the floating assistant acts it out (T-22).
|
|
205
206
|
useEffect(() => {
|
|
206
207
|
onPresence?.(presenceNow);
|
|
208
|
+
turnFeedRef.current?.setPresence(presenceNow);
|
|
207
209
|
}, [onPresence, presenceNow]);
|
|
208
210
|
// What it last said, for a host that says it outside the conversation —
|
|
209
211
|
// the embed's floating chrome (LOOP R-01): its balloon and its blink.
|
|
@@ -9,9 +9,14 @@ export interface ChatPresence {
|
|
|
9
9
|
face?: string;
|
|
10
10
|
/** What it says before anyone writes. */
|
|
11
11
|
welcome?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Its deployment is paused (LOOP R-17): it says so beside its face, and
|
|
14
|
+
* its assistant dozes (T-08, T-22), whatever its last turn left behind.
|
|
15
|
+
*/
|
|
16
|
+
paused?: boolean;
|
|
12
17
|
/**
|
|
13
18
|
* Told what it is doing as it changes — idle, thinking, working, waiting
|
|
14
|
-
* for you — for a host that draws the face itself, in a frame of its own.
|
|
19
|
+
* for you, paused — for a host that draws the face itself, in a frame of its own.
|
|
15
20
|
* Read once, when the workspace is built: pass a stable function, such as
|
|
16
21
|
* a state setter.
|
|
17
22
|
*/
|
|
@@ -17,6 +17,7 @@ import { type ReadonlySignal } from '@datalayer/reactor';
|
|
|
17
17
|
import type { ContextSnapshotData } from '../../../types';
|
|
18
18
|
import type { DisplayItem } from '../../../types/chat';
|
|
19
19
|
import type { ChatMessage } from '../../../types/messages';
|
|
20
|
+
import type { PresenceState } from '../../../chat/presence/presenceStatus';
|
|
20
21
|
import type { ChatTurnSnapshot, ChatTurnStatus, ConversationEntry } from '../../core';
|
|
21
22
|
export type TurnFeed = {
|
|
22
23
|
/** The turn, for readers. */
|
|
@@ -38,6 +39,10 @@ export type TurnFeed = {
|
|
|
38
39
|
conversation: ReadonlySignal<ConversationEntry[]>;
|
|
39
40
|
/** The chat's items as they now stand, streaming included. */
|
|
40
41
|
items: (items: DisplayItem[]) => void;
|
|
42
|
+
/** What the application is doing, as its chat says it (T-08), for readers. */
|
|
43
|
+
presence: ReadonlySignal<PresenceState>;
|
|
44
|
+
/** What the application is doing now. */
|
|
45
|
+
setPresence: (state: PresenceState) => void;
|
|
41
46
|
};
|
|
42
47
|
/** A message's words, whether its content is a string or parts. */
|
|
43
48
|
export declare function messageText(message: ChatMessage): string;
|
|
@@ -65,9 +65,16 @@ const IDLE = { id: 0, status: 'idle' };
|
|
|
65
65
|
export function createTurnFeed() {
|
|
66
66
|
const turn = signal(IDLE);
|
|
67
67
|
const conversation = signal([]);
|
|
68
|
+
const presence = signal('idle');
|
|
68
69
|
return {
|
|
69
70
|
turn,
|
|
70
71
|
conversation,
|
|
72
|
+
presence,
|
|
73
|
+
setPresence: state => {
|
|
74
|
+
if (presence.value !== state) {
|
|
75
|
+
presence.value = state;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
71
78
|
items: items => {
|
|
72
79
|
const next = conversationOf(items);
|
|
73
80
|
// Nothing new: readers do not re-render for an identical conversation.
|
|
@@ -4,7 +4,7 @@ import { type ThemeVariant } from '@datalayer/primer-addons';
|
|
|
4
4
|
export declare const REFERENCE_SCREENS: readonly ["conversation", "beside-work", "worker-activity", "approval"];
|
|
5
5
|
export type ReferenceScreen = (typeof REFERENCE_SCREENS)[number];
|
|
6
6
|
/** The floating assistant's pictures (T-27): its states, and stepped aside. */
|
|
7
|
-
export declare const ASSISTANT_PICTURES: readonly ["idle", "thinking", "working", "waiting", "speaking", "aside"];
|
|
7
|
+
export declare const ASSISTANT_PICTURES: readonly ["idle", "thinking", "working", "waiting", "paused", "speaking", "aside"];
|
|
8
8
|
export type AssistantPicture = (typeof ASSISTANT_PICTURES)[number];
|
|
9
9
|
/**
|
|
10
10
|
* The characters pictured idle, each in light and dark (T-25, T-24):
|
|
@@ -26,7 +26,8 @@ import { ChatBaseHeader } from '../../chat/header/ChatHeaderBase';
|
|
|
26
26
|
import { ChatMessageList } from '../../chat/messages/ChatMessageList';
|
|
27
27
|
import { InputPrompt } from '../../chat/prompt/InputPrompt';
|
|
28
28
|
import { PresenceFace, PresenceLine } from '../../chat/presence/Presence';
|
|
29
|
-
import { AssistantStage } from '../../chat/assistant/AssistantStage';
|
|
29
|
+
import { AssistantStage, } from '../../chat/assistant/AssistantStage';
|
|
30
|
+
import { ASSISTANT_WORDS, } from '../../chat/assistant/state';
|
|
30
31
|
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
31
32
|
import { AssistantCharactersPlugin, assistantCharacterNamed, } from '../../loop/plugins/assistant-characters';
|
|
32
33
|
import { OwlCharacterPlugin } from '../../examples/utils/owlCharacterPlugin';
|
|
@@ -43,6 +44,7 @@ export const ASSISTANT_PICTURES = [
|
|
|
43
44
|
'thinking',
|
|
44
45
|
'working',
|
|
45
46
|
'waiting',
|
|
47
|
+
'paused',
|
|
46
48
|
'speaking',
|
|
47
49
|
'aside',
|
|
48
50
|
];
|
|
@@ -210,7 +212,19 @@ const BALLOONS = {
|
|
|
210
212
|
text: 'Three customers wrote about late deliveries this week: Ada, Grace and Alan.',
|
|
211
213
|
more: true,
|
|
212
214
|
},
|
|
213
|
-
|
|
215
|
+
// An approval it waits on, answered in the balloon (T-23).
|
|
216
|
+
waiting: {
|
|
217
|
+
text: ASSISTANT_WORDS.approval,
|
|
218
|
+
approval: {
|
|
219
|
+
id: 'picture',
|
|
220
|
+
asks: 'send_reply',
|
|
221
|
+
why: 'Send a reply to a customer: ask me first',
|
|
222
|
+
others: 1,
|
|
223
|
+
onApprove: () => undefined,
|
|
224
|
+
onDeny: () => undefined,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
paused: { text: ASSISTANT_WORDS.paused },
|
|
214
228
|
};
|
|
215
229
|
/**
|
|
216
230
|
* The paper clip in one state, in the page's corner. `aside` puts a dialog
|