@datalayer/agent-runtimes 1.3.30 → 1.3.33

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 (79) hide show
  1. package/lib/chat/ChatFloating.js +40 -8
  2. package/lib/chat/assistant/AssistantStage.d.ts +26 -7
  3. package/lib/chat/assistant/AssistantStage.js +115 -8
  4. package/lib/chat/assistant/SpeechBalloon.js +7 -3
  5. package/lib/chat/assistant/characters.d.ts +32 -1
  6. package/lib/chat/assistant/characters.js +180 -15
  7. package/lib/chat/assistant/state.d.ts +28 -0
  8. package/lib/chat/assistant/state.js +31 -0
  9. package/lib/chat/base/ChatBase.js +34 -19
  10. package/lib/chat/base/modelChoice.d.ts +27 -1
  11. package/lib/chat/base/modelChoice.js +29 -15
  12. package/lib/chat/prompt/InputPrompt.d.ts +7 -1
  13. package/lib/chat/prompt/InputPrompt.js +2 -2
  14. package/lib/chat/prompt/InputPromptBase.js +1 -1
  15. package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
  16. package/lib/chat/prompt/menus/ModelSelector.js +17 -5
  17. package/lib/chat/useViewportDrag.d.ts +9 -1
  18. package/lib/chat/useViewportDrag.js +4 -4
  19. package/lib/components/a2ui/index.d.ts +2 -0
  20. package/lib/components/a2ui/index.js +2 -0
  21. package/lib/components/a2ui/visibility.d.ts +15 -0
  22. package/lib/components/a2ui/visibility.js +24 -0
  23. package/lib/components/a2ui/visibleWhen.d.ts +17 -0
  24. package/lib/components/a2ui/visibleWhen.js +54 -0
  25. package/lib/examples/ChatAssistantExample.d.ts +4 -0
  26. package/lib/examples/ChatAssistantExample.js +26 -5
  27. package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
  28. package/lib/examples/utils/owlCharacterPlugin.js +44 -0
  29. package/lib/loop/apps/AppFeedback.d.ts +27 -0
  30. package/lib/loop/apps/AppFeedback.js +78 -0
  31. package/lib/loop/apps/AppRenderer.js +17 -2
  32. package/lib/loop/apps/appspec.js +6 -3
  33. package/lib/loop/apps/checks.d.ts +10 -2
  34. package/lib/loop/apps/checks.js +21 -10
  35. package/lib/loop/apps/feedback.d.ts +51 -0
  36. package/lib/loop/apps/feedback.js +61 -0
  37. package/lib/loop/apps/index.d.ts +2 -0
  38. package/lib/loop/apps/index.js +2 -0
  39. package/lib/loop/apps/rules.d.ts +14 -0
  40. package/lib/loop/apps/rules.js +24 -0
  41. package/lib/loop/core/canvasBlocks.d.ts +16 -0
  42. package/lib/loop/core/canvasBlocks.js +13 -0
  43. package/lib/loop/core/index.d.ts +7 -0
  44. package/lib/loop/core/index.js +3 -0
  45. package/lib/loop/index.d.ts +1 -1
  46. package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
  47. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +4 -2
  48. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +3 -4
  49. package/lib/loop/plugins/app-page/AppPage.js +1 -1
  50. package/lib/loop/plugins/app-page/{appPage.d.ts → appPageModel.d.ts} +1 -1
  51. package/lib/loop/plugins/app-page/index.d.ts +1 -1
  52. package/lib/loop/plugins/app-page/index.js +2 -2
  53. package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
  54. package/lib/loop/plugins/canvas-blocks/index.js +118 -0
  55. package/lib/loop/plugins/chat/ChatView.js +7 -3
  56. package/lib/loop/plugins/chat/turnState.d.ts +5 -2
  57. package/lib/loop/plugins/chat/turnState.js +2 -1
  58. package/lib/specs/actions.js +1 -0
  59. package/lib/specs/agents/agents.js +16 -4
  60. package/lib/specs/apps.js +21 -10
  61. package/lib/specs/appspecSchema.js +13 -7
  62. package/lib/specs/models.d.ts +1 -1
  63. package/lib/specs/models.js +8 -8
  64. package/lib/specs/tools.d.ts +1 -0
  65. package/lib/specs/tools.js +19 -0
  66. package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
  67. package/lib/stories/loop/LoopReference.stories.js +31 -0
  68. package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
  69. package/lib/stories/loop/ReferenceScreens.js +256 -0
  70. package/lib/stories/loop/pictures-main.d.ts +2 -0
  71. package/lib/stories/loop/pictures-main.js +75 -0
  72. package/lib/types/agentspecs.d.ts +6 -1
  73. package/lib/types/chat.d.ts +5 -0
  74. package/lib/types/config.d.ts +8 -0
  75. package/package.json +9 -6
  76. package/scripts/check-cloudflare-models.py +2 -2
  77. package/scripts/codegen/generate_apps.py +2 -1
  78. package/scripts/codegen/generate_models.py +4 -4
  79. /package/lib/loop/plugins/app-page/{appPage.js → appPageModel.js} +0 -0
@@ -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[];
@@ -1,33 +1,198 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- const INK = '#1D1D1F';
2
+ import { loopColors } from '@datalayer/primer-addons/lib/theme';
3
+ const INK = loopColors.ink;
4
+ const INK_DARK = loopColors.inkDark;
3
5
  /** Two eyes with lids that blink and pupils that look about. */
4
- function Eyes({ cx, cy, gap, r, }) {
6
+ function Eyes({ cx, cy, gap, r, colours, }) {
5
7
  const left = cx - gap / 2;
6
8
  const right = cx + gap / 2;
7
- return (_jsxs("g", { children: [_jsx("circle", { cx: left, cy: cy, r: r, fill: "#FFFFFF", stroke: INK, strokeWidth: 1.5 }), _jsx("circle", { cx: right, cy: cy, r: r, fill: "#FFFFFF", stroke: INK, strokeWidth: 1.5 }), _jsxs("g", { className: "assistant-pupils", children: [_jsx("circle", { cx: left + r * 0.15, cy: cy + r * 0.1, r: r * 0.45, fill: INK }), _jsx("circle", { cx: right + r * 0.15, cy: cy + r * 0.1, r: r * 0.45, fill: INK }), _jsx("circle", { cx: left + r * 0.3, cy: cy - r * 0.1, r: r * 0.14, fill: "#FFFFFF" }), _jsx("circle", { cx: right + r * 0.3, cy: cy - r * 0.1, r: r * 0.14, fill: "#FFFFFF" })] }), _jsxs("g", { className: "assistant-lids", children: [_jsx("ellipse", { cx: left, cy: cy, rx: r + 0.5, ry: r + 0.5, fill: "currentColor" }), _jsx("ellipse", { cx: right, cy: cy, rx: r + 0.5, ry: r + 0.5, fill: "currentColor" })] })] }));
9
+ return (_jsxs("g", { children: [_jsx("circle", { cx: left, cy: cy, r: r, fill: colours.white, stroke: colours.ring, strokeWidth: 1.5 }), _jsx("circle", { cx: right, cy: cy, r: r, fill: colours.white, stroke: colours.ring, strokeWidth: 1.5 }), _jsxs("g", { className: "assistant-pupils", children: [_jsx("circle", { cx: left + r * 0.15, cy: cy + r * 0.1, r: r * 0.45, fill: colours.pupil }), _jsx("circle", { cx: right + r * 0.15, cy: cy + r * 0.1, r: r * 0.45, fill: colours.pupil }), _jsx("circle", { cx: left + r * 0.3, cy: cy - r * 0.1, r: r * 0.14, fill: colours.white }), _jsx("circle", { cx: right + r * 0.3, cy: cy - r * 0.1, r: r * 0.14, fill: colours.white })] }), _jsxs("g", { className: "assistant-lids", children: [_jsx("ellipse", { cx: left, cy: cy, rx: r + 0.5, ry: r + 0.5, fill: colours.lid }), _jsx("ellipse", { cx: right, cy: cy, rx: r + 0.5, ry: r + 0.5, fill: colours.lid })] })] }));
8
10
  }
11
+ /**
12
+ * Each drawing's colours, light and dark. The dark ones are drawn for the
13
+ * `loop` theme's dark page (near-black, `loopColors.black`) and not the light
14
+ * drawing on black: lines that were ink turn to the dark mode's ink, the
15
+ * steel and the violets go lighter, outlines that a light page needs give
16
+ * way to the fill's own edge.
17
+ */
18
+ const PAPER_CLIP = {
19
+ light: {
20
+ wire: '#7A808A',
21
+ shine: '#E3E6EB',
22
+ line: INK,
23
+ eye: { white: loopColors.white, ring: INK, pupil: INK, lid: '#B8BDC6' },
24
+ },
25
+ dark: {
26
+ wire: '#C9CED6',
27
+ shine: '#8D93A0',
28
+ line: INK_DARK,
29
+ eye: {
30
+ white: INK_DARK,
31
+ ring: loopColors.grayDark,
32
+ pupil: INK,
33
+ lid: '#8D93A0',
34
+ },
35
+ },
36
+ };
37
+ const WIZARD = {
38
+ light: {
39
+ hat: '#6E5CD6',
40
+ brim: '#4A3C9E',
41
+ robe: '#5B4BB7',
42
+ face: '#F2D3B3',
43
+ faceEdge: '#A0694A',
44
+ beard: '#F4F4F6',
45
+ beardEdge: '#7A808A',
46
+ star: '#F8D469',
47
+ eye: { white: loopColors.white, ring: INK, pupil: INK, lid: '#F2D3B3' },
48
+ },
49
+ dark: {
50
+ hat: '#9C88F0',
51
+ brim: '#7B67DB',
52
+ robe: '#8F7CE8',
53
+ face: '#E8C4A0',
54
+ faceEdge: '#E8C4A0',
55
+ beard: '#E4E4EA',
56
+ beardEdge: loopColors.grayDark,
57
+ star: '#F8D469',
58
+ eye: { white: INK_DARK, ring: INK, pupil: INK, lid: '#E8C4A0' },
59
+ },
60
+ };
61
+ const CAT = {
62
+ light: {
63
+ fur: '#F4A261',
64
+ edge: '#B4571A',
65
+ tail: '#E08A45',
66
+ ear: '#F6A5C1',
67
+ nose: '#B83A6B',
68
+ whiskers: INK,
69
+ eye: { white: loopColors.white, ring: INK, pupil: INK, lid: '#F4A261' },
70
+ },
71
+ dark: {
72
+ fur: '#F4A261',
73
+ edge: '#F4A261',
74
+ tail: '#E08A45',
75
+ ear: '#F6A5C1',
76
+ nose: '#B83A6B',
77
+ whiskers: INK_DARK,
78
+ eye: { white: INK_DARK, ring: INK, pupil: INK, lid: '#F4A261' },
79
+ },
80
+ };
81
+ const LOOP_EYES = {
82
+ light: {
83
+ eye: { white: loopColors.white, ring: INK, pupil: INK, lid: '#E3E6EB' },
84
+ },
85
+ dark: {
86
+ eye: {
87
+ white: INK_DARK,
88
+ ring: INK_DARK,
89
+ pupil: loopColors.black,
90
+ lid: loopColors.grayDark,
91
+ },
92
+ },
93
+ };
9
94
  /** A paper clip of our own: a bent wire with eyes on its upper loop. */
10
- function PaperClip({ size }) {
11
- return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Paper clip", style: { color: '#B8BDC6' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M40 86 L40 30 Q40 14 52 14 Q64 14 64 30 L64 74 Q64 82 56 82 Q48 82 48 74 L48 36", fill: "none", stroke: "#9AA0AA", strokeWidth: 6, strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M40 86 L40 30 Q40 14 52 14 Q64 14 64 30 L64 74 Q64 82 56 82 Q48 82 48 74 L48 36", fill: "none", stroke: "#E3E6EB", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M36 22 Q42 18 47 21", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M57 21 Q62 18 68 22", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx(Eyes, { cx: 52, cy: 30, gap: 16, r: 6.5 }), _jsx("path", { className: "assistant-mouth", d: "M48 44 Q52 47 56 44", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" })] }) }));
95
+ function PaperClip({ size, mode = 'light', }) {
96
+ const c = PAPER_CLIP[mode];
97
+ const wire = 'M40 86 L40 30 Q40 14 52 14 Q64 14 64 30 L64 74 Q64 82 56 82 Q48 82 48 74 L48 36';
98
+ return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Paper clip", "data-assistant-mode": mode, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: wire, fill: "none", stroke: c.wire, strokeWidth: 6, strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: wire, fill: "none", stroke: c.shine, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M36 22 Q42 18 47 21", stroke: c.line, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M57 21 Q62 18 68 22", stroke: c.line, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx(Eyes, { cx: 52, cy: 30, gap: 16, r: 6.5, colours: c.eye }), _jsx("path", { className: "assistant-mouth", d: "M48 44 Q52 47 56 44", stroke: c.line, strokeWidth: 2, fill: "none", strokeLinecap: "round" })] }) }));
12
99
  }
13
100
  /** A wizard: a starred hat, a white beard, kind eyes. */
14
- function Wizard({ size }) {
15
- return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Wizard", style: { color: '#F2D3B3' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M24 92 Q48 58 72 92 Z", fill: "#5B4BB7" }), _jsx("circle", { cx: 48, cy: 48, r: 17, fill: "#F2D3B3" }), _jsx("path", { d: "M31 50 Q48 92 65 50 Q57 60 48 60 Q39 60 31 50 Z", fill: "#F4F4F6", stroke: "#D9DAE0", strokeWidth: 1 }), _jsx("path", { d: "M22 36 L48 2 L74 36 Q48 30 22 36 Z", fill: "#6E5CD6" }), _jsx("path", { d: "M20 37 Q48 29 76 37 Q48 43 20 37 Z", fill: "#4A3C9E" }), _jsx("path", { d: "M52 14 l2 4 4 1 -3 3 1 4 -4 -2 -4 2 1 -4 -3 -3 4 -1 Z", fill: "#F8D469" }), _jsx("circle", { cx: 40, cy: 26, r: 1.8, fill: "#F8D469" }), _jsx(Eyes, { cx: 48, cy: 46, gap: 13, r: 5 }), _jsx("path", { className: "assistant-mouth", d: "M44 58 Q48 61 52 58", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" })] }) }));
101
+ function Wizard({ size, mode = 'light', }) {
102
+ const c = WIZARD[mode];
103
+ return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Wizard", "data-assistant-mode": mode, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M24 92 Q48 58 72 92 Z", fill: c.robe }), _jsx("circle", { cx: 48, cy: 48, r: 17, fill: c.face, stroke: c.faceEdge, strokeWidth: 1.5 }), _jsx("path", { d: "M31 50 Q48 92 65 50 Q57 60 48 60 Q39 60 31 50 Z", fill: c.beard, stroke: c.beardEdge, strokeWidth: 1.2 }), _jsx("path", { d: "M22 36 L48 2 L74 36 Q48 30 22 36 Z", fill: c.hat }), _jsx("path", { d: "M20 37 Q48 29 76 37 Q48 43 20 37 Z", fill: c.brim }), _jsx("path", { d: "M52 14 l2 4 4 1 -3 3 1 4 -4 -2 -4 2 1 -4 -3 -3 4 -1 Z", fill: c.star }), _jsx("circle", { cx: 40, cy: 26, r: 1.8, fill: c.star }), _jsx(Eyes, { cx: 48, cy: 46, gap: 13, r: 5, colours: c.eye }), _jsx("path", { className: "assistant-mouth", d: "M44 58 Q48 61 52 58", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" })] }) }));
16
104
  }
17
105
  /** A cat: round, two ears, whiskers, a tail. */
18
- function Cat({ size }) {
19
- return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Cat", style: { color: '#F4A261' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M70 84 Q90 78 84 58", stroke: "#E08A45", strokeWidth: 6, fill: "none", strokeLinecap: "round" }), _jsx("ellipse", { cx: 48, cy: 78, rx: 24, ry: 14, fill: "#F4A261" }), _jsx("path", { d: "M24 30 L28 8 L42 22 Z", fill: "#F4A261" }), _jsx("path", { d: "M72 30 L68 8 L54 22 Z", fill: "#F4A261" }), _jsx("path", { d: "M28 26 L30 14 L38 22 Z", fill: "#F6A5C1" }), _jsx("path", { d: "M68 26 L66 14 L58 22 Z", fill: "#F6A5C1" }), _jsx("circle", { cx: 48, cy: 42, r: 24, fill: "#F4A261" }), _jsx(Eyes, { cx: 48, cy: 38, gap: 18, r: 6.5 }), _jsx("path", { d: "M45 48 L51 48 L48 52 Z", fill: "#B83A6B" }), _jsx("path", { className: "assistant-mouth", d: "M43 55 Q48 58 53 55", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M30 48 L18 46 M30 52 L18 54 M66 48 L78 46 M66 52 L78 54", stroke: INK, strokeWidth: 1.2, strokeLinecap: "round" })] }) }));
106
+ function Cat({ size, mode = 'light', }) {
107
+ const c = CAT[mode];
108
+ const tail = 'M70 84 Q90 78 84 58';
109
+ return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Cat", "data-assistant-mode": mode, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: tail, stroke: c.edge, strokeWidth: 8, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: tail, stroke: c.tail, strokeWidth: 5, fill: "none", strokeLinecap: "round" }), _jsx("ellipse", { cx: 48, cy: 78, rx: 24, ry: 14, fill: c.fur, stroke: c.edge, strokeWidth: 1.5 }), _jsx("path", { d: "M24 30 L28 8 L42 22 Z", fill: c.fur, stroke: c.edge, strokeWidth: 1.5, strokeLinejoin: "round" }), _jsx("path", { d: "M72 30 L68 8 L54 22 Z", fill: c.fur, stroke: c.edge, strokeWidth: 1.5, strokeLinejoin: "round" }), _jsx("path", { d: "M28 26 L30 14 L38 22 Z", fill: c.ear }), _jsx("path", { d: "M68 26 L66 14 L58 22 Z", fill: c.ear }), _jsx("circle", { cx: 48, cy: 42, r: 24, fill: c.fur, stroke: c.edge, strokeWidth: 1.5 }), _jsx(Eyes, { cx: 48, cy: 38, gap: 18, r: 6.5, colours: c.eye }), _jsx("path", { d: "M45 48 L51 48 L48 52 Z", fill: c.nose }), _jsx("path", { className: "assistant-mouth", d: "M43 55 Q48 58 53 55", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M30 48 L18 46 M30 52 L18 54 M66 48 L78 46 M66 52 L78 54", stroke: c.whiskers, strokeWidth: 1.2, strokeLinecap: "round" })] }) }));
20
110
  }
21
111
  /** The L👀P eyes: the product's own face, two eyes and nothing else. */
22
- function LoopEyes({ size }) {
23
- return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "L\uD83D\uDC40P eyes", style: { color: '#FFFFFF' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx(Eyes, { cx: 48, cy: 50, gap: 40, r: 18 }), _jsx("path", { className: "assistant-mouth", d: "M42 80 Q48 84 54 80", stroke: INK, strokeWidth: 2.5, fill: "none", strokeLinecap: "round", opacity: 0 })] }) }));
112
+ function LoopEyes({ size, mode = 'light', }) {
113
+ const c = LOOP_EYES[mode];
114
+ return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "L\uD83D\uDC40P eyes", "data-assistant-mode": mode, children: _jsxs("g", { className: "assistant-body", children: [_jsx(Eyes, { cx: 48, cy: 50, gap: 40, r: 18, colours: c.eye }), _jsx("path", { className: "assistant-mouth", d: "M42 80 Q48 84 54 80", stroke: mode === 'dark' ? INK_DARK : INK, strokeWidth: 2.5, fill: "none", strokeLinecap: "round", opacity: 0 })] }) }));
24
115
  }
116
+ const eyeOf = (eye, on) => ({
117
+ white: eye.white,
118
+ ring: eye.ring,
119
+ pupil: eye.pupil,
120
+ on,
121
+ });
25
122
  /** The characters Datalayer ships, in the order a picker shows them. */
26
123
  export const ASSISTANT_CHARACTERS = [
27
- { id: 'paperclip', name: 'Paper clip', Drawing: PaperClip },
28
- { id: 'wizard', name: 'Wizard', Drawing: Wizard },
29
- { id: 'cat', name: 'Cat', Drawing: Cat },
30
- { id: 'eyes', name: 'L👀P eyes', Drawing: LoopEyes },
124
+ {
125
+ id: 'paperclip',
126
+ name: 'Paper clip',
127
+ Drawing: PaperClip,
128
+ keyColours: {
129
+ light: {
130
+ edges: [PAPER_CLIP.light.wire, PAPER_CLIP.light.line],
131
+ eye: eyeOf(PAPER_CLIP.light.eye, 'page'),
132
+ },
133
+ dark: {
134
+ edges: [PAPER_CLIP.dark.wire, PAPER_CLIP.dark.line],
135
+ eye: eyeOf(PAPER_CLIP.dark.eye, 'page'),
136
+ },
137
+ },
138
+ },
139
+ {
140
+ id: 'wizard',
141
+ name: 'Wizard',
142
+ Drawing: Wizard,
143
+ keyColours: {
144
+ light: {
145
+ edges: [
146
+ WIZARD.light.hat,
147
+ WIZARD.light.brim,
148
+ WIZARD.light.robe,
149
+ WIZARD.light.faceEdge,
150
+ WIZARD.light.beardEdge,
151
+ ],
152
+ eye: eyeOf(WIZARD.light.eye, WIZARD.light.face),
153
+ },
154
+ dark: {
155
+ edges: [
156
+ WIZARD.dark.hat,
157
+ WIZARD.dark.brim,
158
+ WIZARD.dark.robe,
159
+ WIZARD.dark.faceEdge,
160
+ WIZARD.dark.beardEdge,
161
+ ],
162
+ eye: eyeOf(WIZARD.dark.eye, WIZARD.dark.face),
163
+ },
164
+ },
165
+ },
166
+ {
167
+ id: 'cat',
168
+ name: 'Cat',
169
+ Drawing: Cat,
170
+ keyColours: {
171
+ light: {
172
+ edges: [CAT.light.edge, CAT.light.whiskers],
173
+ eye: eyeOf(CAT.light.eye, CAT.light.fur),
174
+ },
175
+ dark: {
176
+ edges: [CAT.dark.edge, CAT.dark.whiskers],
177
+ eye: eyeOf(CAT.dark.eye, CAT.dark.fur),
178
+ },
179
+ },
180
+ },
181
+ {
182
+ id: 'eyes',
183
+ name: 'L👀P eyes',
184
+ Drawing: LoopEyes,
185
+ keyColours: {
186
+ light: {
187
+ edges: [LOOP_EYES.light.eye.ring],
188
+ eye: eyeOf(LOOP_EYES.light.eye, 'page'),
189
+ },
190
+ dark: {
191
+ edges: [LOOP_EYES.dark.eye.white],
192
+ eye: eyeOf(LOOP_EYES.dark.eye, 'page'),
193
+ },
194
+ },
195
+ },
31
196
  ];
32
197
  /** The character an assistant shows when none is chosen. */
33
198
  export const DEFAULT_ASSISTANT_CHARACTER = 'paperclip';