@datalayer/agent-runtimes 1.3.30 → 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/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 +34 -19
- package/lib/chat/base/modelChoice.d.ts +27 -1
- package/lib/chat/base/modelChoice.js +29 -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.js +17 -2
- 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 +2 -0
- package/lib/loop/apps/index.js +2 -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 +7 -0
- package/lib/loop/core/index.js +3 -0
- package/lib/loop/index.d.ts +1 -1
- package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +4 -2
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +3 -4
- package/lib/loop/plugins/app-page/AppPage.js +1 -1
- package/lib/loop/plugins/app-page/{appPage.d.ts → appPageModel.d.ts} +1 -1
- package/lib/loop/plugins/app-page/index.d.ts +1 -1
- package/lib/loop/plugins/app-page/index.js +2 -2
- 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 +7 -3
- package/lib/loop/plugins/chat/turnState.d.ts +5 -2
- package/lib/loop/plugins/chat/turnState.js +2 -1
- package/lib/specs/actions.js +1 -0
- package/lib/specs/agents/agents.js +16 -4
- package/lib/specs/apps.js +21 -10
- package/lib/specs/appspecSchema.js +13 -7
- 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 +6 -1
- package/lib/types/chat.d.ts +5 -0
- package/lib/types/config.d.ts +8 -0
- package/package.json +9 -6
- package/scripts/check-cloudflare-models.py +2 -2
- package/scripts/codegen/generate_apps.py +2 -1
- package/scripts/codegen/generate_models.py +4 -4
- /package/lib/loop/plugins/app-page/{appPage.js → appPageModel.js} +0 -0
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[];
|
|
@@ -1,33 +1,198 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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';
|