@datalayer/agent-runtimes 1.3.29 → 1.3.32
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/AgentNode.js +14 -33
- package/lib/chat/ChatFloating.js +40 -8
- package/lib/chat/assistant/AssistantStage.d.ts +26 -7
- package/lib/chat/assistant/AssistantStage.js +115 -8
- package/lib/chat/assistant/SpeechBalloon.js +7 -3
- package/lib/chat/assistant/characters.d.ts +32 -1
- package/lib/chat/assistant/characters.js +180 -15
- package/lib/chat/assistant/state.d.ts +28 -0
- package/lib/chat/assistant/state.js +31 -0
- package/lib/chat/base/ChatBase.js +74 -50
- package/lib/chat/base/modelChoice.d.ts +40 -1
- package/lib/chat/base/modelChoice.js +42 -15
- package/lib/chat/prompt/InputPrompt.d.ts +7 -1
- package/lib/chat/prompt/InputPrompt.js +2 -2
- package/lib/chat/prompt/InputPromptBase.js +1 -1
- package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
- package/lib/chat/prompt/menus/ModelSelector.js +17 -5
- package/lib/chat/useViewportDrag.d.ts +9 -1
- package/lib/chat/useViewportDrag.js +4 -4
- package/lib/components/a2ui/index.d.ts +2 -0
- package/lib/components/a2ui/index.js +2 -0
- package/lib/components/a2ui/visibility.d.ts +15 -0
- package/lib/components/a2ui/visibility.js +24 -0
- package/lib/components/a2ui/visibleWhen.d.ts +17 -0
- package/lib/components/a2ui/visibleWhen.js +54 -0
- package/lib/examples/ChatAssistantExample.d.ts +4 -0
- package/lib/examples/ChatAssistantExample.js +26 -5
- package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
- package/lib/examples/utils/owlCharacterPlugin.js +44 -0
- package/lib/loop/apps/AppFeedback.d.ts +27 -0
- package/lib/loop/apps/AppFeedback.js +78 -0
- package/lib/loop/apps/AppRenderer.d.ts +33 -1
- package/lib/loop/apps/AppRenderer.js +111 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/appspec.js +6 -3
- package/lib/loop/apps/checks.d.ts +10 -2
- package/lib/loop/apps/checks.js +21 -10
- package/lib/loop/apps/feedback.d.ts +51 -0
- package/lib/loop/apps/feedback.js +61 -0
- package/lib/loop/apps/index.d.ts +3 -0
- package/lib/loop/apps/index.js +3 -0
- package/lib/loop/apps/rules.d.ts +14 -0
- package/lib/loop/apps/rules.js +24 -0
- package/lib/loop/core/canvasBlocks.d.ts +16 -0
- package/lib/loop/core/canvasBlocks.js +13 -0
- package/lib/loop/core/index.d.ts +15 -0
- package/lib/loop/core/index.js +3 -0
- package/lib/loop/embed/AppEmbed.d.ts +42 -0
- package/lib/loop/embed/AppEmbed.js +205 -0
- package/lib/loop/embed/datalayer-app.d.ts +1 -0
- package/lib/loop/embed/datalayer-app.js +16 -0
- package/lib/loop/embed/element.d.ts +57 -0
- package/lib/loop/embed/element.js +405 -0
- package/lib/loop/embed/embedConfig.d.ts +120 -0
- package/lib/loop/embed/embedConfig.js +148 -0
- package/lib/loop/embed/embedTheme.d.ts +24 -0
- package/lib/loop/embed/embedTheme.js +83 -0
- package/lib/loop/index.d.ts +6 -2
- package/lib/loop/index.js +8 -2
- package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +23 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +36 -9
- package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -1
- package/lib/loop/plugins/agents/switchable.d.ts +7 -0
- package/lib/loop/plugins/agents/switchable.js +91 -88
- package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
- package/lib/loop/plugins/app-page/AppPage.js +53 -0
- package/lib/loop/plugins/app-page/appPageModel.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPageModel.js +443 -0
- package/lib/loop/plugins/app-page/index.d.ts +8 -0
- package/lib/loop/plugins/app-page/index.js +34 -0
- package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
- package/lib/loop/plugins/canvas-blocks/index.js +118 -0
- package/lib/loop/plugins/chat/ChatView.js +52 -26
- package/lib/loop/plugins/chat/turnState.d.ts +5 -2
- package/lib/loop/plugins/chat/turnState.js +2 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
- package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
- package/lib/loop/plugins/page-layout/index.d.ts +13 -0
- package/lib/loop/plugins/page-layout/index.js +10 -1
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +9 -0
- package/lib/specs/agents/agents.js +20 -4
- package/lib/specs/apps.d.ts +10 -1
- package/lib/specs/apps.js +1779 -154
- package/lib/specs/appspecSchema.js +14 -8
- package/lib/specs/models.d.ts +1 -1
- package/lib/specs/models.js +8 -8
- package/lib/specs/tools.d.ts +1 -0
- package/lib/specs/tools.js +19 -0
- package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
- package/lib/stories/loop/LoopReference.stories.js +31 -0
- package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
- package/lib/stories/loop/ReferenceScreens.js +256 -0
- package/lib/stories/loop/pictures-main.d.ts +1 -0
- package/lib/stories/loop/pictures-main.js +64 -0
- package/lib/types/agentspecs.d.ts +17 -1
- package/lib/types/chat.d.ts +5 -0
- package/lib/types/config.d.ts +20 -0
- package/package.json +10 -6
- package/scripts/check-cloudflare-models.py +2 -2
- package/scripts/codegen/generate_agents.py +50 -2
- package/scripts/codegen/generate_apps.py +38 -7
- package/scripts/codegen/generate_models.py +4 -4
package/lib/AgentNode.js
CHANGED
|
@@ -345,7 +345,8 @@ export function AgentNode() {
|
|
|
345
345
|
]);
|
|
346
346
|
const [inferenceProvider, setInferenceProvider] = useState('datalayer');
|
|
347
347
|
const [inferenceModels, setInferenceModels] = useState([]);
|
|
348
|
-
|
|
348
|
+
// The runtime's sentence about its models, or why it gave none.
|
|
349
|
+
const [inferenceModelsNote, setInferenceModelsNote] = useState(null);
|
|
349
350
|
const [configurationLoaded, setConfigurationLoaded] = useState(false);
|
|
350
351
|
const [isActiveAgentRunning, setIsActiveAgentRunning] = useState(false);
|
|
351
352
|
// The protocol the active agent actually speaks, as reported by the node.
|
|
@@ -712,49 +713,26 @@ export function AgentNode() {
|
|
|
712
713
|
useEffect(() => {
|
|
713
714
|
if (inferenceProvider !== 'datalayer') {
|
|
714
715
|
setInferenceModels([]);
|
|
715
|
-
|
|
716
|
+
setInferenceModelsNote(null);
|
|
716
717
|
return;
|
|
717
718
|
}
|
|
719
|
+
// The runtime's answer, as it had it from ai-inference: no list of its
|
|
720
|
+
// own when that answer is empty or did not come — the sentence says why.
|
|
718
721
|
const loadInferenceModels = async () => {
|
|
719
722
|
try {
|
|
720
723
|
const response = await fetch(`${AGENT_RUNTIMES_BASE_URL}/api/v1/configure/inference/models`);
|
|
721
724
|
if (!response.ok) {
|
|
722
725
|
setInferenceModels([]);
|
|
723
|
-
|
|
726
|
+
setInferenceModelsNote(`The runtime did not list its models (${response.status}).`);
|
|
724
727
|
return;
|
|
725
728
|
}
|
|
726
729
|
const payload = await response.json();
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
: [];
|
|
730
|
-
const fromBedrock = Array.isArray(payload.bedrock_anthropic_models)
|
|
731
|
-
? payload.bedrock_anthropic_models.filter(Boolean)
|
|
732
|
-
: [];
|
|
733
|
-
const fallback = [
|
|
734
|
-
'bedrock/us.anthropic.claude-3-5-sonnet-20240620-v1:0',
|
|
735
|
-
'bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
|
736
|
-
'bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0',
|
|
737
|
-
];
|
|
738
|
-
const models = fromModels.length > 0
|
|
739
|
-
? fromModels
|
|
740
|
-
: fromBedrock.length > 0
|
|
741
|
-
? fromBedrock
|
|
742
|
-
: fallback;
|
|
743
|
-
setInferenceModels(models);
|
|
744
|
-
const specDefault = Array.isArray(payload.bedrock_anthropic_model_specs)
|
|
745
|
-
? payload.bedrock_anthropic_model_specs.find(s => s?.default)?.id
|
|
746
|
-
: undefined;
|
|
747
|
-
const selected = (specDefault && models.includes(specDefault) ? specDefault : null) ||
|
|
748
|
-
(payload.default_model && models.includes(payload.default_model)
|
|
749
|
-
? payload.default_model
|
|
750
|
-
: null) ||
|
|
751
|
-
models[0] ||
|
|
752
|
-
null;
|
|
753
|
-
setInferenceDefaultModel(selected);
|
|
730
|
+
setInferenceModels(Array.isArray(payload.models) ? payload.models.filter(Boolean) : []);
|
|
731
|
+
setInferenceModelsNote(payload.note ?? null);
|
|
754
732
|
}
|
|
755
|
-
catch {
|
|
733
|
+
catch (error) {
|
|
756
734
|
setInferenceModels([]);
|
|
757
|
-
|
|
735
|
+
setInferenceModelsNote(`The runtime did not list its models (${String(error)}).`);
|
|
758
736
|
}
|
|
759
737
|
};
|
|
760
738
|
loadInferenceModels();
|
|
@@ -1280,7 +1258,10 @@ export function AgentNode() {
|
|
|
1280
1258
|
display: 'flex',
|
|
1281
1259
|
flexDirection: 'column',
|
|
1282
1260
|
gap: 1,
|
|
1283
|
-
}, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "
|
|
1261
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Models ai-inference serves" }), inferenceModels.length === 0 ? (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: inferenceModelsNote ??
|
|
1262
|
+
'The runtime has not said which models ai-inference serves.' })) : (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { children: inferenceModels.length === 1
|
|
1263
|
+
? inferenceModels[0]
|
|
1264
|
+
: `${inferenceModels.length} models` }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsx(ActionList, { children: inferenceModels.map(model => (_jsx(ActionList.Item, { inactiveText: "Selection is locked", children: model }, model))) }) })] }))] }))] }), iamUser ? (_jsxs(Box, { sx: {
|
|
1284
1265
|
border: '1px solid',
|
|
1285
1266
|
borderColor: 'border.default',
|
|
1286
1267
|
borderRadius: 2,
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -48,6 +48,13 @@ function useIsMobile(breakpoint = 640) {
|
|
|
48
48
|
}, [breakpoint]);
|
|
49
49
|
return isMobile;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* A length in pixels, as a string: a number in `sx` from 0 to 12 is read as
|
|
53
|
+
* the theme's space scale (`left: 8` is 64px).
|
|
54
|
+
*/
|
|
55
|
+
const px = (value) => `${Math.round(value)}px`;
|
|
56
|
+
/** Kept between 8px from the window's start and `max`. */
|
|
57
|
+
const clamp = (value, max) => Math.max(8, Math.min(value, Math.max(8, max)));
|
|
51
58
|
/**
|
|
52
59
|
* ChatFloating component
|
|
53
60
|
* A floating chat window built on ChatBase
|
|
@@ -141,7 +148,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
141
148
|
}
|
|
142
149
|
}, [isOpen, saying]);
|
|
143
150
|
const stageRef = useRef(null);
|
|
144
|
-
const stageDrag = useViewportDrag(stageRef);
|
|
151
|
+
const stageDrag = useViewportDrag(stageRef, { whole: true });
|
|
145
152
|
useEffect(() => {
|
|
146
153
|
if (!assistantShown) {
|
|
147
154
|
return;
|
|
@@ -387,9 +394,28 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
387
394
|
const widthPx = typeof width === 'number' ? width : 400;
|
|
388
395
|
const heightPx = typeof height === 'number' ? height : 550;
|
|
389
396
|
if (stageDrag.position) {
|
|
397
|
+
// Toward the page's inside: above the character, below it, or beside
|
|
398
|
+
// it when the window is too short for either.
|
|
399
|
+
const { left, top } = stageDrag.position;
|
|
400
|
+
const onLeft = left + ASSISTANT_SIZE / 2 < window.innerWidth / 2;
|
|
401
|
+
const above = top - heightPx - 16;
|
|
402
|
+
const below = top + ASSISTANT_SIZE + 16;
|
|
403
|
+
const beside = above < 8 && below + heightPx > window.innerHeight - 8;
|
|
404
|
+
const x = beside
|
|
405
|
+
? onLeft
|
|
406
|
+
? left + ASSISTANT_SIZE + 16
|
|
407
|
+
: left - widthPx - 16
|
|
408
|
+
: onLeft
|
|
409
|
+
? left
|
|
410
|
+
: left + ASSISTANT_SIZE - widthPx;
|
|
411
|
+
const y = beside
|
|
412
|
+
? top + ASSISTANT_SIZE - heightPx
|
|
413
|
+
: above >= 8
|
|
414
|
+
? above
|
|
415
|
+
: below;
|
|
390
416
|
return {
|
|
391
|
-
left:
|
|
392
|
-
top:
|
|
417
|
+
left: px(clamp(x, window.innerWidth - widthPx - 8)),
|
|
418
|
+
top: px(clamp(y, window.innerHeight - heightPx - 8)),
|
|
393
419
|
right: 'auto',
|
|
394
420
|
bottom: 'auto',
|
|
395
421
|
};
|
|
@@ -557,8 +583,8 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
557
583
|
...(viewMode === 'floating-draggable' && !isMobile
|
|
558
584
|
? drag.position
|
|
559
585
|
? {
|
|
560
|
-
left: drag.position.left,
|
|
561
|
-
top: drag.position.top,
|
|
586
|
+
left: px(drag.position.left),
|
|
587
|
+
top: px(drag.position.top),
|
|
562
588
|
right: 'auto',
|
|
563
589
|
bottom: 'auto',
|
|
564
590
|
}
|
|
@@ -694,7 +720,10 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
694
720
|
* person's attention, until the chat is opened; hovered, they show in a
|
|
695
721
|
* balloon above it in place of the tooltip.
|
|
696
722
|
*/
|
|
697
|
-
const chatButton = (_jsx(IconButton, { icon: buttonIcon ? buttonIcon : CommentDiscussionIcon, "aria-label": buttonTooltip,
|
|
723
|
+
const chatButton = (_jsx(IconButton, { icon: buttonIcon ? buttonIcon : CommentDiscussionIcon, "aria-label": buttonTooltip,
|
|
724
|
+
// With words to say, the balloon stands in for the tooltip: Primer's
|
|
725
|
+
// own would cover it.
|
|
726
|
+
unsafeDisableTooltip: !!saying, onClick: assistantMode && assistantAway === 'page'
|
|
698
727
|
? callAssistantBack
|
|
699
728
|
: handleToggle, size: "large", sx: {
|
|
700
729
|
width: 56,
|
|
@@ -729,8 +758,11 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
729
758
|
},
|
|
730
759
|
} }));
|
|
731
760
|
return (_jsxs(_Fragment, { children: [assistantShown && (_jsx(AssistantStage, { character: assistantCharacter, state: assistantState, size: ASSISTANT_SIZE, place: stageDrag.position
|
|
732
|
-
? {
|
|
733
|
-
|
|
761
|
+
? {
|
|
762
|
+
left: px(stageDrag.position.left),
|
|
763
|
+
top: px(stageDrag.position.top),
|
|
764
|
+
}
|
|
765
|
+
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
|
|
734
766
|
...getPositionStyles(),
|
|
735
767
|
}, children: _jsxs(Box, { sx: {
|
|
736
768
|
position: 'relative',
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* a corner, acting out what the application is doing, with a balloon that
|
|
4
4
|
* says a word while the conversation is closed. Clicked, it opens the
|
|
5
5
|
* conversation; dragged, it moves, and the conversation follows; sent away,
|
|
6
|
-
* it says goodbye and leaves a small way back.
|
|
6
|
+
* it says goodbye and leaves a small way back. It never sits over an open
|
|
7
|
+
* dialog, an overlay or another chat's composer, nor where the pointer is
|
|
8
|
+
* working: it steps aside — out of sight and out of the pointer's way — until
|
|
9
|
+
* the place is clear again.
|
|
7
10
|
*
|
|
8
11
|
* Its motions are one set for every character, by the parts each drawing
|
|
9
12
|
* names (`assistant-body`, `-pupils`, `-lids`, `-mouth`), and nothing moves
|
|
@@ -14,7 +17,7 @@
|
|
|
14
17
|
import type { JSX, RefObject } from 'react';
|
|
15
18
|
import { type AssistantCharacter } from './characters';
|
|
16
19
|
import type { AssistantCharacterData } from './formats/types';
|
|
17
|
-
import type
|
|
20
|
+
import { type AssistantAway, type AssistantState } from './state';
|
|
18
21
|
/**
|
|
19
22
|
* Where the balloon goes so that it stays inside the window (T-23): toward
|
|
20
23
|
* the middle of the page from whichever half the character stands in, and
|
|
@@ -32,6 +35,20 @@ export declare function balloonSide(place: {
|
|
|
32
35
|
side: 'above' | 'below';
|
|
33
36
|
align: 'left' | 'right';
|
|
34
37
|
};
|
|
38
|
+
/** Why the assistant has stepped aside, if it has (T-27). */
|
|
39
|
+
export type AssistantAside = 'obstacle' | 'pointer' | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the assistant steps aside (T-27): while an open dialog, an overlay
|
|
42
|
+
* or another chat's composer is under it (`obstacle`), and while the pointer
|
|
43
|
+
* works over or beside it — pressing, or dragging — until it has been away for
|
|
44
|
+
* `POINTER_CALM_MS` (`pointer`). Its own conversation, `own`, is neither, and
|
|
45
|
+
* nothing counts while `paused` (its own menu is open).
|
|
46
|
+
*
|
|
47
|
+
* The page is looked over on a short timer rather than watched: a dialog
|
|
48
|
+
* opens, a composer scrolls or a chat is dragged under it without a mutation
|
|
49
|
+
* this could hear, and a few rectangles every 400ms cost nothing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function useKeepClear(stageRef: RefObject<HTMLElement | null>, own: RefObject<HTMLElement | null> | undefined, paused: boolean): AssistantAside;
|
|
35
52
|
export interface AssistantStageProps {
|
|
36
53
|
/** The character: one Datalayer ships, by id (T-25), or one loaded from a file (T-26). */
|
|
37
54
|
character: string | AssistantCharacter | AssistantCharacterData;
|
|
@@ -40,10 +57,7 @@ export interface AssistantStageProps {
|
|
|
40
57
|
/** Its size, in pixels. */
|
|
41
58
|
size?: number;
|
|
42
59
|
/** Where it sits once moved, or the corner it starts in. */
|
|
43
|
-
place:
|
|
44
|
-
left: number;
|
|
45
|
-
top: number;
|
|
46
|
-
} | React.CSSProperties;
|
|
60
|
+
place: React.CSSProperties;
|
|
47
61
|
/** The element the drag measures. */
|
|
48
62
|
stageRef: RefObject<HTMLDivElement | null>;
|
|
49
63
|
/** Starts a drag. */
|
|
@@ -65,6 +79,11 @@ export interface AssistantStageProps {
|
|
|
65
79
|
insist?: boolean;
|
|
66
80
|
/** Send it away (T-27): for the page, for the session, or for good. */
|
|
67
81
|
onDismiss: (away: AssistantAway) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Its own conversation's window: a dialog or composer there is not one it
|
|
84
|
+
* steps aside for, nor is the pointer working there (T-27).
|
|
85
|
+
*/
|
|
86
|
+
ownRef?: RefObject<HTMLElement | null>;
|
|
68
87
|
}
|
|
69
|
-
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, }: AssistantStageProps): JSX.Element;
|
|
88
|
+
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, }: AssistantStageProps): JSX.Element;
|
|
70
89
|
export default AssistantStage;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState } from 'react';
|
|
3
|
-
import { ActionList, ActionMenu, IconButton } from '@primer/react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ActionList, ActionMenu, IconButton, useTheme } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
5
5
|
import { XIcon } from '@primer/octicons-react';
|
|
6
6
|
import { assistantCharacter } from './characters';
|
|
7
7
|
import { SpeechBalloon } from './SpeechBalloon';
|
|
8
8
|
import { SpriteCharacter } from './SpriteCharacter';
|
|
9
|
+
import { ASSISTANT_OBSTACLES, POINTER_CALM_MS, boxesMeet, pointerNear, } from './state';
|
|
9
10
|
/** The motions, by the state the stage is in. */
|
|
10
11
|
const MOTIONS = {
|
|
11
12
|
'@keyframes assistantFloat': {
|
|
@@ -54,6 +55,14 @@ const AWAY_CHOICES = [
|
|
|
54
55
|
];
|
|
55
56
|
/** Room the balloon needs above the character before it goes below. */
|
|
56
57
|
const BALLOON_ROOM = 200;
|
|
58
|
+
/** A place's length in pixels, given as a number or as `"<n>px"`. */
|
|
59
|
+
function pixels(value) {
|
|
60
|
+
if (typeof value === 'number') {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
const match = typeof value === 'string' ? /^(-?\d+(?:\.\d+)?)px$/.exec(value) : null;
|
|
64
|
+
return match ? Number(match[1]) : undefined;
|
|
65
|
+
}
|
|
57
66
|
/**
|
|
58
67
|
* Where the balloon goes so that it stays inside the window (T-23): toward
|
|
59
68
|
* the middle of the page from whichever half the character stands in, and
|
|
@@ -63,8 +72,8 @@ export function balloonSide(place, viewport = {
|
|
|
63
72
|
width: typeof window === 'undefined' ? 1280 : window.innerWidth,
|
|
64
73
|
height: typeof window === 'undefined' ? 800 : window.innerHeight,
|
|
65
74
|
}) {
|
|
66
|
-
const left =
|
|
67
|
-
const top =
|
|
75
|
+
const left = pixels(place.left);
|
|
76
|
+
const top = pixels(place.top);
|
|
68
77
|
const align = left !== undefined
|
|
69
78
|
? left < viewport.width / 2
|
|
70
79
|
? 'left'
|
|
@@ -81,7 +90,93 @@ export function balloonSide(place, viewport = {
|
|
|
81
90
|
: 'above';
|
|
82
91
|
return { side, align };
|
|
83
92
|
}
|
|
84
|
-
|
|
93
|
+
/** How often the page is looked over for what the assistant must not cover. */
|
|
94
|
+
const CLEAR_CHECK_MS = 400;
|
|
95
|
+
/**
|
|
96
|
+
* Whether the assistant steps aside (T-27): while an open dialog, an overlay
|
|
97
|
+
* or another chat's composer is under it (`obstacle`), and while the pointer
|
|
98
|
+
* works over or beside it — pressing, or dragging — until it has been away for
|
|
99
|
+
* `POINTER_CALM_MS` (`pointer`). Its own conversation, `own`, is neither, and
|
|
100
|
+
* nothing counts while `paused` (its own menu is open).
|
|
101
|
+
*
|
|
102
|
+
* The page is looked over on a short timer rather than watched: a dialog
|
|
103
|
+
* opens, a composer scrolls or a chat is dragged under it without a mutation
|
|
104
|
+
* this could hear, and a few rectangles every 400ms cost nothing.
|
|
105
|
+
*/
|
|
106
|
+
export function useKeepClear(stageRef, own, paused) {
|
|
107
|
+
const [obstructed, setObstructed] = useState(false);
|
|
108
|
+
const [pointerBusy, setPointerBusy] = useState(false);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (paused) {
|
|
111
|
+
setObstructed(false);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const check = () => {
|
|
115
|
+
const stage = stageRef.current;
|
|
116
|
+
if (!stage) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const box = stage.getBoundingClientRect();
|
|
120
|
+
const mine = own?.current;
|
|
121
|
+
const hit = Array.from(document.querySelectorAll(ASSISTANT_OBSTACLES)).some(element => {
|
|
122
|
+
if (stage.contains(element) ||
|
|
123
|
+
element.contains(stage) ||
|
|
124
|
+
mine?.contains(element)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const other = element.getBoundingClientRect();
|
|
128
|
+
return other.width > 0 && other.height > 0 && boxesMeet(box, other);
|
|
129
|
+
});
|
|
130
|
+
setObstructed(hit);
|
|
131
|
+
};
|
|
132
|
+
check();
|
|
133
|
+
const timer = window.setInterval(check, CLEAR_CHECK_MS);
|
|
134
|
+
return () => window.clearInterval(timer);
|
|
135
|
+
}, [stageRef, own, paused]);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (paused) {
|
|
138
|
+
setPointerBusy(false);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let calm;
|
|
142
|
+
let aside = false;
|
|
143
|
+
const onPointer = (event) => {
|
|
144
|
+
const stage = stageRef.current;
|
|
145
|
+
const target = event.target;
|
|
146
|
+
if (!stage ||
|
|
147
|
+
(target && (stage.contains(target) || own?.current?.contains(target)))) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// A pointer passing by is not working; once aside, any move near keeps
|
|
151
|
+
// it aside, so it does not come back under a pointer still there.
|
|
152
|
+
if (event.type === 'pointermove' && event.buttons === 0 && !aside) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!pointerNear(stage.getBoundingClientRect(), {
|
|
156
|
+
x: event.clientX,
|
|
157
|
+
y: event.clientY,
|
|
158
|
+
})) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
aside = true;
|
|
162
|
+
setPointerBusy(true);
|
|
163
|
+
clearTimeout(calm);
|
|
164
|
+
calm = setTimeout(() => {
|
|
165
|
+
aside = false;
|
|
166
|
+
setPointerBusy(false);
|
|
167
|
+
}, POINTER_CALM_MS);
|
|
168
|
+
};
|
|
169
|
+
document.addEventListener('pointerdown', onPointer, true);
|
|
170
|
+
document.addEventListener('pointermove', onPointer, true);
|
|
171
|
+
return () => {
|
|
172
|
+
clearTimeout(calm);
|
|
173
|
+
document.removeEventListener('pointerdown', onPointer, true);
|
|
174
|
+
document.removeEventListener('pointermove', onPointer, true);
|
|
175
|
+
};
|
|
176
|
+
}, [stageRef, own, paused]);
|
|
177
|
+
return obstructed ? 'obstacle' : pointerBusy ? 'pointer' : undefined;
|
|
178
|
+
}
|
|
179
|
+
export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, }) {
|
|
85
180
|
// A shipped one by id, a drawing contributed by a plugin (T-24), or a
|
|
86
181
|
// character read from a file (T-26).
|
|
87
182
|
const shipped = typeof character === 'string'
|
|
@@ -92,17 +187,29 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
92
187
|
const name = shipped
|
|
93
188
|
? shipped.name
|
|
94
189
|
: character.name;
|
|
190
|
+
// Drawn for the chat's colour mode: the dark drawing on a dark page (T-25).
|
|
191
|
+
const { colorScheme } = useTheme();
|
|
192
|
+
const colorMode = colorScheme?.startsWith('dark') ? 'dark' : 'light';
|
|
95
193
|
const [hovered, setHovered] = useState(false);
|
|
96
194
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
97
195
|
// Where the press began: a press that moves is a drag, not a click.
|
|
98
196
|
const pressedAt = useRef(null);
|
|
99
|
-
const
|
|
100
|
-
|
|
197
|
+
const aside = useKeepClear(stageRef, ownRef, menuOpen);
|
|
198
|
+
const showBalloon = !aside && !open && !!balloon && (hovered || insist);
|
|
199
|
+
return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, "data-assistant-aside": aside, sx: {
|
|
101
200
|
position: 'fixed',
|
|
102
201
|
zIndex: 1002,
|
|
103
202
|
...place,
|
|
104
203
|
width: size,
|
|
105
204
|
height: size,
|
|
205
|
+
// Aside (T-27): out of sight, out of the tab order and let through
|
|
206
|
+
// to what is under it; back as soon as the place is clear.
|
|
207
|
+
opacity: aside ? 0 : 1,
|
|
208
|
+
visibility: aside ? 'hidden' : 'visible',
|
|
209
|
+
pointerEvents: aside ? 'none' : undefined,
|
|
210
|
+
transition: aside
|
|
211
|
+
? 'opacity 150ms ease, visibility 0s linear 150ms'
|
|
212
|
+
: 'opacity 150ms ease',
|
|
106
213
|
...MOTIONS,
|
|
107
214
|
'& .assistant-lids ellipse': {
|
|
108
215
|
transformBox: 'fill-box',
|
|
@@ -183,7 +290,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
183
290
|
outlineOffset: 2,
|
|
184
291
|
borderRadius: '50%',
|
|
185
292
|
},
|
|
186
|
-
}, children: shipped ? (_jsx(shipped.Drawing, { size: size })) : (_jsx(SpriteCharacter, { character: character, state: state, size: size })) }), (hovered || menuOpen) && state !== 'goodbye' && (_jsx(Box, { sx: { position: 'absolute', top: -
|
|
293
|
+
}, children: shipped ? (_jsx(shipped.Drawing, { size: size, mode: colorMode })) : (_jsx(SpriteCharacter, { character: character, state: state, size: size })) }), (hovered || menuOpen) && !aside && state !== 'goodbye' && (_jsx(Box, { sx: { position: 'absolute', top: '-6px', right: '-6px' }, children: _jsxs(ActionMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { icon: XIcon, "aria-label": `Send ${name} away`, size: "small", variant: "invisible", "data-assistant-dismiss": "", sx: {
|
|
187
294
|
bg: 'canvas.default',
|
|
188
295
|
borderRadius: '50%',
|
|
189
296
|
boxShadow: 'shadow.small',
|
|
@@ -4,7 +4,11 @@ import { Box } from '@datalayer/primer-addons';
|
|
|
4
4
|
export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, }) {
|
|
5
5
|
return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
|
|
6
6
|
position: 'absolute',
|
|
7
|
-
|
|
7
|
+
// Pixels, as strings: a number here is read as the theme's space
|
|
8
|
+
// scale (`-7` would be `-space[7]`, 48px).
|
|
9
|
+
...(side === 'above'
|
|
10
|
+
? { bottom: `${above}px` }
|
|
11
|
+
: { top: `${above}px` }),
|
|
8
12
|
[align]: 0,
|
|
9
13
|
maxWidth: 280,
|
|
10
14
|
width: 'max-content',
|
|
@@ -22,8 +26,8 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
22
26
|
'&::after': {
|
|
23
27
|
content: '""',
|
|
24
28
|
position: 'absolute',
|
|
25
|
-
...(side === 'above' ? { bottom: -
|
|
26
|
-
[align]: tailAt - 6
|
|
29
|
+
...(side === 'above' ? { bottom: '-7px' } : { top: '-7px' }),
|
|
30
|
+
[align]: `${tailAt - 6}px`,
|
|
27
31
|
width: 12,
|
|
28
32
|
height: 12,
|
|
29
33
|
bg: 'canvas.default',
|
|
@@ -9,18 +9,49 @@
|
|
|
9
9
|
* state — `assistant-body`, `assistant-pupils`, `assistant-lids`,
|
|
10
10
|
* `assistant-mouth` — so one set of motions serves every character.
|
|
11
11
|
*
|
|
12
|
+
* Each is drawn twice, for the chat's colour mode: a light drawing for the
|
|
13
|
+
* light page and a dark one for the `loop` theme's dark page, by its dark
|
|
14
|
+
* tokens (`loopColors.inkDark`, `grayDark`, `black`). Their edges and eyes
|
|
15
|
+
* are held to 3 to 1 against both pages by a test (`keyColours`).
|
|
16
|
+
*
|
|
12
17
|
* @module chat/assistant/characters
|
|
13
18
|
*/
|
|
14
19
|
import type { JSX } from 'react';
|
|
20
|
+
/** The colour mode a character is drawn for: the chat's own. */
|
|
21
|
+
export type AssistantColorMode = 'light' | 'dark';
|
|
22
|
+
/**
|
|
23
|
+
* What a drawing must keep readable, by mode (T-25, tested as T-15 tests the
|
|
24
|
+
* faces): the colours that meet the page along its edge, and its eyes.
|
|
25
|
+
*/
|
|
26
|
+
export interface AssistantCharacterKeyColours {
|
|
27
|
+
/** Every colour drawn along the character's edge, where it meets the page. */
|
|
28
|
+
edges: string[];
|
|
29
|
+
eye: {
|
|
30
|
+
/** The white of the eye. */
|
|
31
|
+
white: string;
|
|
32
|
+
/** The ring drawn round it. */
|
|
33
|
+
ring: string;
|
|
34
|
+
/** The pupil. */
|
|
35
|
+
pupil: string;
|
|
36
|
+
/** What the eye sits on: the page, or the face's colour. */
|
|
37
|
+
on: 'page' | string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
15
40
|
/** A character Datalayer ships. */
|
|
16
41
|
export interface AssistantCharacter {
|
|
17
42
|
id: string;
|
|
18
43
|
/** Its name, as the picker shows it. */
|
|
19
44
|
name: string;
|
|
20
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The drawing, at the size asked, for the chat's colour mode (light when
|
|
47
|
+
* unsaid); its parts animate by class.
|
|
48
|
+
*/
|
|
21
49
|
Drawing: (props: {
|
|
22
50
|
size: number;
|
|
51
|
+
mode?: AssistantColorMode;
|
|
23
52
|
}) => JSX.Element;
|
|
53
|
+
/** Its key colours in each mode, for the contrast test (T-25). */
|
|
54
|
+
keyColours?: Record<AssistantColorMode, AssistantCharacterKeyColours>;
|
|
24
55
|
}
|
|
25
56
|
/** The characters Datalayer ships, in the order a picker shows them. */
|
|
26
57
|
export declare const ASSISTANT_CHARACTERS: readonly AssistantCharacter[];
|