@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.
Files changed (107) hide show
  1. package/lib/AgentNode.js +14 -33
  2. package/lib/chat/ChatFloating.js +40 -8
  3. package/lib/chat/assistant/AssistantStage.d.ts +26 -7
  4. package/lib/chat/assistant/AssistantStage.js +115 -8
  5. package/lib/chat/assistant/SpeechBalloon.js +7 -3
  6. package/lib/chat/assistant/characters.d.ts +32 -1
  7. package/lib/chat/assistant/characters.js +180 -15
  8. package/lib/chat/assistant/state.d.ts +28 -0
  9. package/lib/chat/assistant/state.js +31 -0
  10. package/lib/chat/base/ChatBase.js +74 -50
  11. package/lib/chat/base/modelChoice.d.ts +40 -1
  12. package/lib/chat/base/modelChoice.js +42 -15
  13. package/lib/chat/prompt/InputPrompt.d.ts +7 -1
  14. package/lib/chat/prompt/InputPrompt.js +2 -2
  15. package/lib/chat/prompt/InputPromptBase.js +1 -1
  16. package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
  17. package/lib/chat/prompt/menus/ModelSelector.js +17 -5
  18. package/lib/chat/useViewportDrag.d.ts +9 -1
  19. package/lib/chat/useViewportDrag.js +4 -4
  20. package/lib/components/a2ui/index.d.ts +2 -0
  21. package/lib/components/a2ui/index.js +2 -0
  22. package/lib/components/a2ui/visibility.d.ts +15 -0
  23. package/lib/components/a2ui/visibility.js +24 -0
  24. package/lib/components/a2ui/visibleWhen.d.ts +17 -0
  25. package/lib/components/a2ui/visibleWhen.js +54 -0
  26. package/lib/examples/ChatAssistantExample.d.ts +4 -0
  27. package/lib/examples/ChatAssistantExample.js +26 -5
  28. package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
  29. package/lib/examples/utils/owlCharacterPlugin.js +44 -0
  30. package/lib/loop/apps/AppFeedback.d.ts +27 -0
  31. package/lib/loop/apps/AppFeedback.js +78 -0
  32. package/lib/loop/apps/AppRenderer.d.ts +33 -1
  33. package/lib/loop/apps/AppRenderer.js +111 -29
  34. package/lib/loop/apps/agent.d.ts +15 -0
  35. package/lib/loop/apps/agent.js +42 -0
  36. package/lib/loop/apps/appspec.js +6 -3
  37. package/lib/loop/apps/checks.d.ts +10 -2
  38. package/lib/loop/apps/checks.js +21 -10
  39. package/lib/loop/apps/feedback.d.ts +51 -0
  40. package/lib/loop/apps/feedback.js +61 -0
  41. package/lib/loop/apps/index.d.ts +3 -0
  42. package/lib/loop/apps/index.js +3 -0
  43. package/lib/loop/apps/rules.d.ts +14 -0
  44. package/lib/loop/apps/rules.js +24 -0
  45. package/lib/loop/core/canvasBlocks.d.ts +16 -0
  46. package/lib/loop/core/canvasBlocks.js +13 -0
  47. package/lib/loop/core/index.d.ts +15 -0
  48. package/lib/loop/core/index.js +3 -0
  49. package/lib/loop/embed/AppEmbed.d.ts +42 -0
  50. package/lib/loop/embed/AppEmbed.js +205 -0
  51. package/lib/loop/embed/datalayer-app.d.ts +1 -0
  52. package/lib/loop/embed/datalayer-app.js +16 -0
  53. package/lib/loop/embed/element.d.ts +57 -0
  54. package/lib/loop/embed/element.js +405 -0
  55. package/lib/loop/embed/embedConfig.d.ts +120 -0
  56. package/lib/loop/embed/embedConfig.js +148 -0
  57. package/lib/loop/embed/embedTheme.d.ts +24 -0
  58. package/lib/loop/embed/embedTheme.js +83 -0
  59. package/lib/loop/index.d.ts +6 -2
  60. package/lib/loop/index.js +8 -2
  61. package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
  62. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +23 -3
  63. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +36 -9
  64. package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
  65. package/lib/loop/plugins/agent-capacity/index.js +3 -1
  66. package/lib/loop/plugins/agents/switchable.d.ts +7 -0
  67. package/lib/loop/plugins/agents/switchable.js +91 -88
  68. package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
  69. package/lib/loop/plugins/app-page/AppPage.js +53 -0
  70. package/lib/loop/plugins/app-page/appPageModel.d.ts +180 -0
  71. package/lib/loop/plugins/app-page/appPageModel.js +443 -0
  72. package/lib/loop/plugins/app-page/index.d.ts +8 -0
  73. package/lib/loop/plugins/app-page/index.js +34 -0
  74. package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
  75. package/lib/loop/plugins/canvas-blocks/index.js +118 -0
  76. package/lib/loop/plugins/chat/ChatView.js +52 -26
  77. package/lib/loop/plugins/chat/turnState.d.ts +5 -2
  78. package/lib/loop/plugins/chat/turnState.js +2 -1
  79. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
  80. package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
  81. package/lib/loop/plugins/page-layout/index.d.ts +13 -0
  82. package/lib/loop/plugins/page-layout/index.js +10 -1
  83. package/lib/loop/presets.d.ts +12 -0
  84. package/lib/loop/presets.js +5 -3
  85. package/lib/specs/actions.js +9 -0
  86. package/lib/specs/agents/agents.js +20 -4
  87. package/lib/specs/apps.d.ts +10 -1
  88. package/lib/specs/apps.js +1779 -154
  89. package/lib/specs/appspecSchema.js +14 -8
  90. package/lib/specs/models.d.ts +1 -1
  91. package/lib/specs/models.js +8 -8
  92. package/lib/specs/tools.d.ts +1 -0
  93. package/lib/specs/tools.js +19 -0
  94. package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
  95. package/lib/stories/loop/LoopReference.stories.js +31 -0
  96. package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
  97. package/lib/stories/loop/ReferenceScreens.js +256 -0
  98. package/lib/stories/loop/pictures-main.d.ts +1 -0
  99. package/lib/stories/loop/pictures-main.js +64 -0
  100. package/lib/types/agentspecs.d.ts +17 -1
  101. package/lib/types/chat.d.ts +5 -0
  102. package/lib/types/config.d.ts +20 -0
  103. package/package.json +10 -6
  104. package/scripts/check-cloudflare-models.py +2 -2
  105. package/scripts/codegen/generate_agents.py +50 -2
  106. package/scripts/codegen/generate_apps.py +38 -7
  107. 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
- const [inferenceDefaultModel, setInferenceDefaultModel] = useState(null);
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
- setInferenceDefaultModel(null);
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
- setInferenceDefaultModel(null);
726
+ setInferenceModelsNote(`The runtime did not list its models (${response.status}).`);
724
727
  return;
725
728
  }
726
729
  const payload = await response.json();
727
- const fromModels = Array.isArray(payload.models)
728
- ? payload.models.filter(Boolean)
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
- setInferenceDefaultModel(null);
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: "Bedrock Anthropic model" }), inferenceModels.length === 0 ? (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "No model list available." })) : (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { children: inferenceDefaultModel || inferenceModels[0] }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsx(ActionList, { selectionVariant: "single", children: inferenceModels.map(model => (_jsx(ActionList.Item, { selected: inferenceDefaultModel === model, inactiveText: "Selection is locked", children: model }, model))) }) })] }))] }))] }), iamUser ? (_jsxs(Box, { sx: {
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,
@@ -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: Math.max(8, stageDrag.position.left + ASSISTANT_SIZE - widthPx),
392
- top: Math.max(8, stageDrag.position.top - heightPx - 16),
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, onClick: assistantMode && assistantAway === 'page'
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
- ? { left: stageDrag.position.left, top: stageDrag.position.top }
733
- : getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
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 { AssistantAway, AssistantState } from './state';
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 = typeof place.left === 'number' ? place.left : undefined;
67
- const top = typeof place.top === 'number' ? place.top : undefined;
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
- export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, }) {
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 showBalloon = !open && !!balloon && (hovered || insist);
100
- return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, sx: {
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: -6, right: -6 }, 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: {
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
- ...(side === 'above' ? { bottom: above } : { top: above }),
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: -7 } : { top: -7 }),
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
- /** The drawing, at the size asked; its parts animate by class. */
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[];