@datalayer/agent-runtimes 1.3.62 → 1.3.64

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 (64) hide show
  1. package/lib/chat/ChatFloating.d.ts +9 -1
  2. package/lib/chat/ChatFloating.js +69 -18
  3. package/lib/chat/assistant/AssistantStage.d.ts +14 -1
  4. package/lib/chat/assistant/AssistantStage.js +53 -1
  5. package/lib/chat/assistant/SpriteCharacter.js +1 -1
  6. package/lib/chat/assistant/characters.js +1 -5
  7. package/lib/chat/assistant/state.d.ts +7 -1
  8. package/lib/chat/assistant/state.js +3 -2
  9. package/lib/chat/base/ChatBase.js +32 -4
  10. package/lib/chat/messages/ChatMessageList.d.ts +0 -6
  11. package/lib/chat/messages/ChatMessageList.js +8 -2
  12. package/lib/config/AgentConfiguration.js +0 -6
  13. package/lib/examples/AgentA2ATeamExample.js +19 -2
  14. package/lib/examples/ChatAssistantExample.d.ts +3 -1
  15. package/lib/examples/ChatAssistantExample.js +75 -7
  16. package/lib/examples/ChatAssistantGalleryExample.d.ts +3 -2
  17. package/lib/examples/ChatAssistantGalleryExample.js +11 -6
  18. package/lib/examples/DecksAgent.js +4 -2
  19. package/lib/examples/LoopShellExample.js +4 -2
  20. package/lib/examples/VoiceChatExample.d.ts +20 -0
  21. package/lib/examples/VoiceChatExample.js +64 -0
  22. package/lib/examples/example-selector.js +1 -0
  23. package/lib/examples/main.js +12 -7
  24. package/lib/examples/utils/clippyJsCharacters.d.ts +18 -0
  25. package/lib/examples/utils/clippyJsCharacters.js +129 -0
  26. package/lib/loop/apps/appspec.d.ts +3 -1
  27. package/lib/loop/apps/appspec.js +31 -0
  28. package/lib/loop/apps/checks.js +1 -1
  29. package/lib/protocols/VercelAIAdapter.js +5 -0
  30. package/lib/specs/apps.js +112 -0
  31. package/lib/specs/appspecSchema.js +65 -0
  32. package/lib/specs/index.d.ts +1 -0
  33. package/lib/specs/index.js +1 -0
  34. package/lib/specs/voices.d.ts +72 -0
  35. package/lib/specs/voices.js +344 -0
  36. package/lib/types/agents.d.ts +1 -1
  37. package/lib/types/agentspecs.d.ts +16 -0
  38. package/lib/types/chat.d.ts +11 -0
  39. package/lib/voice/VoiceInput.d.ts +35 -0
  40. package/lib/voice/VoiceInput.js +233 -0
  41. package/lib/voice/capture.d.ts +30 -0
  42. package/lib/voice/capture.js +113 -0
  43. package/lib/voice/consent.d.ts +6 -0
  44. package/lib/voice/consent.js +34 -0
  45. package/lib/voice/hearing.d.ts +37 -0
  46. package/lib/voice/hearing.js +168 -0
  47. package/lib/voice/index.d.ts +47 -0
  48. package/lib/voice/index.js +13 -0
  49. package/lib/voice/pinned.d.ts +28 -0
  50. package/lib/voice/pinned.js +75 -0
  51. package/lib/voice/sentences.d.ts +26 -0
  52. package/lib/voice/sentences.js +71 -0
  53. package/lib/voice/speaker.d.ts +66 -0
  54. package/lib/voice/speaker.js +168 -0
  55. package/lib/voice/types.d.ts +92 -0
  56. package/lib/voice/types.js +5 -0
  57. package/lib/voice/useSpokenAnswers.d.ts +12 -0
  58. package/lib/voice/useSpokenAnswers.js +90 -0
  59. package/package.json +7 -3
  60. package/scripts/codegen/generate_voices.py +279 -0
  61. package/scripts/voice/measure.py +244 -0
  62. package/scripts/voice/pin_store.py +159 -0
  63. package/scripts/voice/serve_store.py +70 -0
  64. package/scripts/voice/transcribe.mjs +58 -0
@@ -16,6 +16,7 @@
16
16
  * @module chat/ChatFloating
17
17
  */
18
18
  import React from 'react';
19
+ import type { ChatVoice } from '../voice';
19
20
  import { type AssistantCharacter } from './assistant/characters';
20
21
  import type { AssistantCharacterData } from './assistant/formats/types';
21
22
  import { type AssistantSaying } from './assistant/state';
@@ -149,10 +150,17 @@ export interface ChatFloatingProps extends ChatCommonProps {
149
150
  * own props (`protocol`, `suggestions`, …) are then not read.
150
151
  */
151
152
  conversation?: FloatingConversation;
153
+ /**
154
+ * Its voice (VOICE.md V1): push-to-talk in the composer, heard on the
155
+ * device; with `output: 'always'`, its answers said by Datalayer's speech
156
+ * service as they are written, the assistant's mouth moving with the sound
157
+ * and its balloon showing the sentence being said.
158
+ */
159
+ voice?: ChatVoice;
152
160
  }
153
161
  /**
154
162
  * ChatFloating component
155
163
  * A floating chat window built on ChatBase
156
164
  */
157
- export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, decisions, }: ChatFloatingProps): React.JSX.Element;
165
+ export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, decisions, voice, }: ChatFloatingProps): React.JSX.Element;
158
166
  export default ChatFloating;
@@ -33,6 +33,8 @@ import { disabledChatViewModes, resolveMountPoint } from './viewModes';
33
33
  import { AssistantStage } from './assistant/AssistantStage';
34
34
  import { decisionsAskerAt } from './assistant/decisions';
35
35
  import { DecisionAsk } from './assistant/DecisionAsk';
36
+ import { ServerSpeaker } from '../voice/speaker';
37
+ import { useSpokenAnswers } from '../voice/useSpokenAnswers';
36
38
  import { BalloonApprovalMessage, CONVERSATION_BALLOON_WIDTH, ConversationBalloonClose, balloonTailAt, conversationBalloonHeight, conversationBalloonSx, peekLine, } from './assistant/ConversationBalloon';
37
39
  import { SpeechBalloon } from './assistant/SpeechBalloon';
38
40
  import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
@@ -66,7 +68,7 @@ const clamp = (value, max) => Math.max(8, Math.min(value, Math.max(8, max)));
66
68
  * ChatFloating component
67
69
  * A floating chat window built on ChatBase
68
70
  */
69
- export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, decisions, }) {
71
+ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, decisions, voice, }) {
70
72
  // Store-based state
71
73
  const storeIsOpen = useChatOpen();
72
74
  const storeMessages = useChatMessages();
@@ -129,6 +131,47 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
129
131
  pendingApproval: false,
130
132
  });
131
133
  const [ownAnswering, setAnswering] = useState(false);
134
+ /*
135
+ * Its voice (VOICE.md V1): the answers said by the speech service, as
136
+ * they are written (VO-20), the state following the sound (VO-22).
137
+ */
138
+ const [voiceItems, setVoiceItems] = useState([]);
139
+ const [speakerState, setSpeakerState] = useState({
140
+ speaking: false,
141
+ });
142
+ const speaker = useMemo(() => voice && voice.output === 'always' && voice.speechUrl
143
+ ? new ServerSpeaker({
144
+ speechUrl: voice.speechUrl,
145
+ voice: voice.voice,
146
+ language: voice.language,
147
+ token: voice.token,
148
+ onState: setSpeakerState,
149
+ })
150
+ : undefined, [voice?.output, voice?.speechUrl]);
151
+ useEffect(() => {
152
+ speaker?.update({
153
+ voice: voice?.voice,
154
+ language: voice?.language,
155
+ token: voice?.token,
156
+ });
157
+ }, [speaker, voice?.voice, voice?.language, voice?.token]);
158
+ useEffect(() => () => speaker?.close(), [speaker]);
159
+ // A browser plays only what a gesture allowed: the first one does.
160
+ useEffect(() => {
161
+ if (!speaker) {
162
+ return;
163
+ }
164
+ const unlock = () => speaker.unlock();
165
+ window.addEventListener('pointerdown', unlock, { once: true });
166
+ window.addEventListener('keydown', unlock, { once: true });
167
+ return () => {
168
+ window.removeEventListener('pointerdown', unlock);
169
+ window.removeEventListener('keydown', unlock);
170
+ };
171
+ }, [speaker]);
172
+ useSpokenAnswers(voiceItems, chatBusy, !!speaker, speaker);
173
+ const stopSpeaking = useCallback(() => speaker?.stop(), [speaker]);
174
+ const mouthLevel = useMemo(() => (speaker ? () => speaker.level() : undefined), [speaker]);
132
175
  const [arriving, setArriving] = useState(false);
133
176
  const [leaving, setLeaving] = useState(false);
134
177
  const [assistantAway, setAssistantAway] = useState(keptAway);
@@ -173,6 +216,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
173
216
  arriving,
174
217
  leaving,
175
218
  speaking: answering,
219
+ voicing: speakerState.speaking,
176
220
  });
177
221
  const unheard = !!saying && saying.id !== heardId;
178
222
  /*
@@ -202,21 +246,27 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
202
246
  : undefined, [decisions?.serverUrl, decisions?.token]);
203
247
  const assistantBalloon = assistantState === 'paused'
204
248
  ? { text: ASSISTANT_WORDS.paused }
205
- : balloonApproval
206
- ? { text: ASSISTANT_WORDS.approval, approval: balloonApproval }
207
- : assistantState === 'waiting'
208
- ? { text: ASSISTANT_WORDS.waiting }
209
- : unheard && saying
210
- ? {
211
- // A peek: the first words; the rest is in the conversation.
212
- ...peekLine(saying.text),
213
- onDismiss: () => {
214
- setHeardId(saying.id);
215
- setFreshSaying(false);
216
- },
217
- }
218
- : { text: description };
219
- const balloonInsists = assistantState === 'greeting' ||
249
+ : speakerState.sentence
250
+ ? // Heard: the sentence being said is the one in the balloon (VO-26).
251
+ { text: speakerState.sentence }
252
+ : speakerState.refused
253
+ ? { text: speakerState.refused }
254
+ : balloonApproval
255
+ ? { text: ASSISTANT_WORDS.approval, approval: balloonApproval }
256
+ : assistantState === 'waiting'
257
+ ? { text: ASSISTANT_WORDS.waiting }
258
+ : unheard && saying
259
+ ? {
260
+ // A peek: the first words; the rest is in the conversation.
261
+ ...peekLine(saying.text),
262
+ onDismiss: () => {
263
+ setHeardId(saying.id);
264
+ setFreshSaying(false);
265
+ },
266
+ }
267
+ : { text: description };
268
+ const balloonInsists = !!speakerState.sentence ||
269
+ assistantState === 'greeting' ||
220
270
  assistantState === 'waiting' ||
221
271
  !!balloonApproval ||
222
272
  (unheard && (assistantState === 'speaking' || freshSaying));
@@ -233,6 +283,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
233
283
  ? previous
234
284
  : tool);
235
285
  setAnswering(newestIsAnswer(items));
286
+ setVoiceItems(items);
236
287
  const said = latestSaying(items);
237
288
  setSaying(previous => previous?.id === said?.id && previous?.text === said?.text
238
289
  ? previous
@@ -906,7 +957,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
906
957
  ...poweredByProps,
907
958
  }, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
908
959
  showAvatars: true,
909
- }, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: frontendTools, showModelSelector: showModelSelector, availableModels: availableModels, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, showContextRing: showContextRing, runtimeId: runtimeId, historyEndpoint: historyEndpoint, historyAuthToken: historyAuthToken, pendingPrompt: pendingPrompt, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, children: children }))] }));
960
+ }, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: frontendTools, showModelSelector: showModelSelector, availableModels: availableModels, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, showContextRing: showContextRing, runtimeId: runtimeId, historyEndpoint: historyEndpoint, historyAuthToken: historyAuthToken, pendingPrompt: pendingPrompt, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, voice: voice, voiceSpeaking: speakerState.speaking, onStopSpeaking: speaker ? stopSpeaking : undefined, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, children: children }))] }));
910
961
  /*
911
962
  * The floating popup's button. Words not yet heard make it blink for the
912
963
  * person's attention, until the chat is opened; hovered, they show in a
@@ -954,7 +1005,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
954
1005
  left: px(stageDrag.position.left),
955
1006
  top: px(stageDrag.position.top),
956
1007
  }
957
- : getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
1008
+ : getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef, mouthLevel: mouthLevel })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
958
1009
  ...getPositionStyles(),
959
1010
  }, children: _jsxs(Box, { sx: {
960
1011
  position: 'relative',
@@ -94,6 +94,19 @@ export interface AssistantStageProps {
94
94
  * decision*, and stays while its form or its answer is on screen.
95
95
  */
96
96
  decide?: DecisionAsker;
97
+ /**
98
+ * How loud its voice is now, between 0 and 1, while it is heard (VOICE.md
99
+ * VO-23): the mouth opens with the sound, in three openings, instead of
100
+ * the talking loop. Still, for a reader who asks for reduced motion.
101
+ */
102
+ mouthLevel?: () => number;
97
103
  }
98
- export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, }: AssistantStageProps): JSX.Element;
104
+ /** The openings of the mouth, by level: shut, a little, half, wide. */
105
+ export declare function mouthOpening(level: number): 0 | 1 | 2 | 3;
106
+ /**
107
+ * Moves the mouth with the voice: the opening is written on the stage as
108
+ * `data-assistant-mouth`, each frame, without drawing the character again.
109
+ */
110
+ export declare function useMouth(stageRef: RefObject<HTMLElement | null>, mouthLevel: (() => number) | undefined, speaking: boolean): void;
111
+ export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, mouthLevel, }: AssistantStageProps): JSX.Element;
99
112
  export default AssistantStage;
@@ -180,7 +180,36 @@ export function useKeepClear(stageRef, own, paused) {
180
180
  }, [stageRef, own, paused]);
181
181
  return obstructed ? 'obstacle' : pointerBusy ? 'pointer' : undefined;
182
182
  }
183
- export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, }) {
183
+ /** The openings of the mouth, by level: shut, a little, half, wide. */
184
+ export function mouthOpening(level) {
185
+ return level < 0.08 ? 0 : level < 0.3 ? 1 : level < 0.6 ? 2 : 3;
186
+ }
187
+ /**
188
+ * Moves the mouth with the voice: the opening is written on the stage as
189
+ * `data-assistant-mouth`, each frame, without drawing the character again.
190
+ */
191
+ export function useMouth(stageRef, mouthLevel, speaking) {
192
+ useEffect(() => {
193
+ const stage = stageRef.current;
194
+ const still = typeof window !== 'undefined' &&
195
+ !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
196
+ if (!stage || !mouthLevel || !speaking || still) {
197
+ stage?.removeAttribute('data-assistant-mouth');
198
+ return;
199
+ }
200
+ let frame = 0;
201
+ const move = () => {
202
+ stage.setAttribute('data-assistant-mouth', String(mouthOpening(mouthLevel())));
203
+ frame = requestAnimationFrame(move);
204
+ };
205
+ frame = requestAnimationFrame(move);
206
+ return () => {
207
+ cancelAnimationFrame(frame);
208
+ stage.removeAttribute('data-assistant-mouth');
209
+ };
210
+ }, [stageRef, mouthLevel, speaking]);
211
+ }
212
+ export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, mouthLevel, }) {
184
213
  // A shipped one by id, a drawing contributed by a plugin (T-24), or a
185
214
  // character read from a file (T-26).
186
215
  const shipped = typeof character === 'string'
@@ -199,6 +228,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
199
228
  // Where the press began: a press that moves is a drag, not a click.
200
229
  const pressedAt = useRef(null);
201
230
  const aside = useKeepClear(stageRef, ownRef, menuOpen);
231
+ useMouth(stageRef, mouthLevel, state === 'speaking');
202
232
  // A decision being asked, or its answer, keeps the balloon up.
203
233
  const [deciding, setDeciding] = useState(false);
204
234
  const showBalloon = !aside && !open && !!balloon && (hovered || insist || deciding);
@@ -261,6 +291,23 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
261
291
  '&[data-assistant-state="speaking"] .assistant-body': {
262
292
  animation: 'assistantFloat 1.6s ease-in-out infinite',
263
293
  },
294
+ // Heard (VO-23): the mouth opens with the sound, not on a loop.
295
+ '&[data-assistant-mouth] .assistant-mouth': {
296
+ animation: 'none',
297
+ transition: 'transform 60ms linear',
298
+ },
299
+ '&[data-assistant-mouth="0"] .assistant-mouth': {
300
+ transform: 'scaleY(1)',
301
+ },
302
+ '&[data-assistant-mouth="1"] .assistant-mouth': {
303
+ transform: 'scaleY(1.5)',
304
+ },
305
+ '&[data-assistant-mouth="2"] .assistant-mouth': {
306
+ transform: 'scaleY(2.1)',
307
+ },
308
+ '&[data-assistant-mouth="3"] .assistant-mouth': {
309
+ transform: 'scaleY(2.8)',
310
+ },
264
311
  // Paused (R-17): it dozes — its eyes shut, its breath slow, a little
265
312
  // greyed — and, still, it is asleep: the shut eyes are not a motion.
266
313
  '&[data-assistant-state="paused"] .assistant-body': {
@@ -275,6 +322,11 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
275
322
  '&[data-assistant-state="goodbye"] .assistant-body': {
276
323
  animation: 'assistantLeave 0.6s ease-in forwards',
277
324
  },
325
+ // A sprite plays its own goodbye, and leaves as the drawn ones do:
326
+ // a character whose file has no goodbye would otherwise stay.
327
+ '&[data-assistant-state="goodbye"] .assistant-sprite': {
328
+ animation: 'assistantLeave 0.6s ease-in forwards',
329
+ },
278
330
  // One still frame per state for a reader who asks for no motion.
279
331
  '@media (prefers-reduced-motion: reduce)': {
280
332
  '& *': { animation: 'none !important' },
@@ -51,7 +51,7 @@ export function SpriteCharacter({ character, state, size, }) {
51
51
  }, [animation, frame, play, character, state, still]);
52
52
  const { width, height } = character.frameSize;
53
53
  const scale = size / Math.max(width, height);
54
- return (_jsx("span", { role: "img", "aria-label": character.name, "data-sprite-animation": play.name, style: {
54
+ return (_jsx("span", { className: "assistant-sprite", role: "img", "aria-label": character.name, "data-sprite-animation": play.name, style: {
55
55
  position: 'relative',
56
56
  display: 'block',
57
57
  width: size,
@@ -40,7 +40,6 @@ const WIZARD = {
40
40
  brim: '#4A3C9E',
41
41
  robe: '#5B4BB7',
42
42
  face: '#F2D3B3',
43
- faceEdge: '#A0694A',
44
43
  beard: '#F4F4F6',
45
44
  beardEdge: '#7A808A',
46
45
  star: '#F8D469',
@@ -51,7 +50,6 @@ const WIZARD = {
51
50
  brim: '#7B67DB',
52
51
  robe: '#8F7CE8',
53
52
  face: '#E8C4A0',
54
- faceEdge: '#E8C4A0',
55
53
  beard: '#E4E4EA',
56
54
  beardEdge: loopColors.grayDark,
57
55
  star: '#F8D469',
@@ -100,7 +98,7 @@ function PaperClip({ size, mode = 'light', }) {
100
98
  /** A wizard: a starred hat, a white beard, kind eyes. */
101
99
  function Wizard({ size, mode = 'light', }) {
102
100
  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" })] }) }));
101
+ 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: "M34 58 Q48 52 62 58 Q70 74 76 92 Q48 98 20 92 Q26 74 34 58 Z", fill: c.robe }), _jsx("path", { d: "M21 90 Q48 96 75 90 L76 92 Q48 98 20 92 Z", fill: c.brim }), _jsx("circle", { cx: 30, cy: 82, r: 1.6, fill: c.star }), _jsx("circle", { cx: 67, cy: 78, r: 1.3, fill: c.star }), _jsx("ellipse", { cx: 48, cy: 46, rx: 13, ry: 12, fill: c.face }), _jsx("path", { d: "M33 38 Q31 66 40 77 Q48 89 56 77 Q65 66 63 38 Q61 50 56 53 Q48 58 40 53 Q35 50 33 38 Z", fill: c.beard, stroke: c.beardEdge, strokeWidth: 1, strokeLinejoin: "round" }), _jsx("path", { className: "assistant-mouth", d: "M44 59 Q48 62 52 59", stroke: INK, strokeWidth: 1.8, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M39 55 Q44 51 48 54 Q52 51 57 55 Q52 57 48 56 Q44 57 39 55 Z", fill: c.beard, stroke: c.beardEdge, strokeWidth: 0.8 }), _jsx("path", { d: "M25 36 Q35 26 39 14 Q43 2 58 3 Q50 8 53 18 Q58 30 71 36 Q48 30 25 36 Z", fill: c.hat }), _jsx("path", { d: "M18 37 Q48 27 78 37 Q48 45 18 37 Z", fill: c.brim }), _jsx("path", { d: "M47 17 l1.8 3.6 4 .6 -2.9 2.8 .7 4 -3.6 -1.9 -3.6 1.9 .7 -4 -2.9 -2.8 4 -.6 Z", fill: c.star }), _jsx("circle", { cx: 56, cy: 28, r: 1.4, fill: c.star }), _jsx(Eyes, { cx: 48, cy: 46, gap: 11, r: 4, colours: c.eye })] }) }));
104
102
  }
105
103
  /** A cat: round, two ears, whiskers, a tail. */
106
104
  function Cat({ size, mode = 'light', }) {
@@ -146,7 +144,6 @@ export const ASSISTANT_CHARACTERS = [
146
144
  WIZARD.light.hat,
147
145
  WIZARD.light.brim,
148
146
  WIZARD.light.robe,
149
- WIZARD.light.faceEdge,
150
147
  WIZARD.light.beardEdge,
151
148
  ],
152
149
  eye: eyeOf(WIZARD.light.eye, WIZARD.light.face),
@@ -156,7 +153,6 @@ export const ASSISTANT_CHARACTERS = [
156
153
  WIZARD.dark.hat,
157
154
  WIZARD.dark.brim,
158
155
  WIZARD.dark.robe,
159
- WIZARD.dark.faceEdge,
160
156
  WIZARD.dark.beardEdge,
161
157
  ],
162
158
  eye: eyeOf(WIZARD.dark.eye, WIZARD.dark.face),
@@ -17,6 +17,11 @@ export interface AssistantMoment {
17
17
  leaving?: boolean;
18
18
  /** The answer is arriving: words are being written. */
19
19
  speaking?: boolean;
20
+ /**
21
+ * Its answer is being heard: sound is playing (VOICE.md VO-22). It speaks
22
+ * while the audio plays, even after the text has ended.
23
+ */
24
+ voicing?: boolean;
20
25
  }
21
26
  /**
22
27
  * The state to act, from the application's presence and the moment.
@@ -24,7 +29,8 @@ export interface AssistantMoment {
24
29
  * Leaving wins, being short; then paused — a paused application neither
25
30
  * greets nor speaks, it dozes; then arriving; then waiting for the person,
26
31
  * which must never be missed; then speaking over thinking, since words
27
- * arriving are what the person watches; then the presence as it is.
32
+ * arriving are what the person watches — and while its voice is heard, even
33
+ * once the words have all arrived; then the presence as it is.
28
34
  */
29
35
  export declare function assistantStateOf(presence: PresenceState, moment?: AssistantMoment): AssistantState;
30
36
  /**
@@ -8,7 +8,8 @@
8
8
  * Leaving wins, being short; then paused — a paused application neither
9
9
  * greets nor speaks, it dozes; then arriving; then waiting for the person,
10
10
  * which must never be missed; then speaking over thinking, since words
11
- * arriving are what the person watches; then the presence as it is.
11
+ * arriving are what the person watches — and while its voice is heard, even
12
+ * once the words have all arrived; then the presence as it is.
12
13
  */
13
14
  export function assistantStateOf(presence, moment = {}) {
14
15
  if (moment.leaving) {
@@ -23,7 +24,7 @@ export function assistantStateOf(presence, moment = {}) {
23
24
  if (presence === 'waiting') {
24
25
  return 'waiting';
25
26
  }
26
- if (moment.speaking && presence !== 'idle') {
27
+ if ((moment.speaking && presence !== 'idle') || moment.voicing) {
27
28
  return 'speaking';
28
29
  }
29
30
  return presence;
@@ -45,6 +45,8 @@ import { PoweredByTag } from '../display/PoweredByTag';
45
45
  import { EphemeralSurfaceControl } from '../EphemeralSurfaceControl';
46
46
  import { ChatMessageList, } from '../messages/ChatMessageList';
47
47
  import { InputPrompt } from '../prompt/InputPrompt';
48
+ import { DeviceHearing } from '../../voice/hearing';
49
+ import { VoiceInput } from '../../voice/VoiceInput';
48
50
  import { isFloatingChatViewMode } from '../viewModes';
49
51
  import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
50
52
  import { EphemeralNotebook } from '../notebook/EphemeralNotebook';
@@ -509,7 +511,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
509
511
  // Mode selection
510
512
  useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady, onLoadingChange, onContextSnapshot, enableStreaming = false,
511
513
  // Extended props
512
- brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
514
+ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
513
515
  // Tool invocation hooks
514
516
  onToolCallStart, onToolCallComplete,
515
517
  // Identity/Authorization props
@@ -2819,10 +2821,17 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2819
2821
  // ========================================================================
2820
2822
  // handleSend
2821
2823
  // ========================================================================
2824
+ // Voice (VOICE.md V1): what was said and put in the composer, until sent.
2825
+ const spokenRef = useRef(undefined);
2826
+ const hearing = useMemo(() => voice && voice.input !== 'off'
2827
+ ? new DeviceHearing(voice.modelsUrl, voice.engines)
2828
+ : undefined, [voice?.input, voice?.modelsUrl, voice?.engines]);
2822
2829
  const handleSend = useCallback(async (messageOverride,
2823
2830
  // AG-UI's: what goes with the run besides the conversation — an
2824
2831
  // application's page says there what it did (LOOP R-04).
2825
- forwardedProps) => {
2832
+ forwardedPropsGiven,
2833
+ // How the message was heard, when it was said (VOICE.md VO-27).
2834
+ spokenGiven) => {
2826
2835
  const messageContent = (messageOverride ?? input).trim();
2827
2836
  if (!messageContent || isLoading)
2828
2837
  return;
@@ -2831,7 +2840,15 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2831
2840
  stoppedRef.current = false;
2832
2841
  suppressAssistantTextForToolOnlyRef.current =
2833
2842
  isToolCallOnlyPrompt(messageContent);
2834
- const userMessage = createUserMessage(messageContent);
2843
+ // Said, then sent: by the microphone at once, or from the composer it
2844
+ // was put in — the words may have been edited, it was still said.
2845
+ const spoken = spokenGiven ??
2846
+ (messageOverride === undefined ? spokenRef.current : undefined);
2847
+ spokenRef.current = undefined;
2848
+ const forwardedProps = spoken
2849
+ ? { ...(forwardedPropsGiven ?? {}), voice: spoken }
2850
+ : forwardedPropsGiven;
2851
+ const userMessage = createUserMessage(messageContent, spoken ? { ...spoken } : undefined);
2835
2852
  const currentMessages = displayItems.filter((item) => !isToolCallMessage(item));
2836
2853
  const allMessages = [...currentMessages, userMessage];
2837
2854
  setDisplayItems(prev => [...prev, userMessage]);
@@ -3513,7 +3530,18 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3513
3530
  mx: 'auto',
3514
3531
  bg: 'canvas.default',
3515
3532
  }, children: [_jsx(ChatMessageList, { displayItems: welcomedItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }), trailingContent] }));
3516
- const inputToolbar = showInput ? (_jsx(InputPrompt, { input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3533
+ // The microphone, in the composer's footer (VO-10).
3534
+ const voiceControl = hearing && voice ? (_jsx(VoiceInput, { hearing: hearing, language: voice.language, consentKey: voice.consentKey ?? 'chat', disabled: disabled || launching, speaking: voiceSpeaking, onStopSpeaking: onStopSpeaking, onListen: onStopSpeaking, onTranscript: (transcript) => {
3535
+ if (voice.sendWhatISay) {
3536
+ void handleSend(transcript.text, undefined, transcript.metadata);
3537
+ return;
3538
+ }
3539
+ spokenRef.current = transcript.metadata;
3540
+ setInput(input.trim()
3541
+ ? `${input.trim()} ${transcript.text}`
3542
+ : transcript.text);
3543
+ } })) : undefined;
3544
+ const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceControl, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3517
3545
  /*
3518
3546
  The same openers the empty state offers, typed into the box they would
3519
3547
  be typed into.
@@ -1,9 +1,3 @@
1
- /**
2
- * MessageList — Renders the scrollable list of chat messages, tool calls,
3
- * and the typing indicator.
4
- *
5
- * @module chat/messages/MessageList
6
- */
7
1
  import { type ReactNode, type RefObject } from 'react';
8
2
  import { type MarkedMcpServer } from '../marks';
9
3
  import type { DisplayItem, AvatarConfig, RenderToolResult } from '../../types/chat';
@@ -9,6 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
9
9
  *
10
10
  * @module chat/messages/MessageList
11
11
  */
12
+ import { MicrophoneIcon } from '@datalayer/icons-react';
12
13
  import { Fragment, useState, useCallback, useEffect, useMemo, useRef, } from 'react';
13
14
  import { Text, Spinner, Link, Label, RelativeTime } from '@primer/react';
14
15
  import { ArrowRightIcon, BroadcastIcon, ClockIcon, DependabotIcon, PersonIcon, SkipIcon, ToolsIcon, XCircleFillIcon, } from '@primer/octicons-react';
@@ -708,11 +709,16 @@ export function ChatMessageList({ displayItems, isLoading, isStreaming, showLoad
708
709
  // the agent reads — see `formSubmission`.
709
710
  const text = getMessageText(message);
710
711
  const submission = parseFormSubmission(text);
711
- return submission ? (_jsx(FormSubmissionMessage, { submission: submission })) : (_jsx(Text, { sx: {
712
+ return submission ? (_jsx(FormSubmissionMessage, { submission: submission })) : (_jsxs(Text, { sx: {
712
713
  fontSize: 1,
713
714
  whiteSpace: 'pre-wrap',
714
715
  wordBreak: 'break-word',
715
- }, children: text }));
716
+ }, children: [message.metadata?.input === 'voice' ? (_jsx(Box, { as: "span", "data-spoken": "", title: "Said aloud", "aria-label": "Said aloud:", sx: {
717
+ display: 'inline-flex',
718
+ verticalAlign: 'text-bottom',
719
+ mr: 1,
720
+ opacity: 0.85,
721
+ }, children: _jsx(MicrophoneIcon, { size: 14, "aria-hidden": "true" }) })) : null, text] }));
716
722
  })()) : message.live && !getMessageText(message) ? (_jsx(TypingDots, { size: 6 })) : (_jsx(Box, { sx: streamdownMarkdownStyles, children: _jsx(Streamdown, { children: normalizeAssistantMarkdown(getMessageText(message) || (isStreaming ? '...' : '')) }) }))] }), renderMessageFooter ? (_jsx(Box, { sx: {
717
723
  display: 'flex',
718
724
  gap: 2,
@@ -193,12 +193,6 @@ const AGENT_LIBRARIES = [
193
193
  description: 'LangChain agent implementations and tools',
194
194
  disabled: true,
195
195
  },
196
- {
197
- value: 'google-adk',
198
- label: 'Google ADK',
199
- description: 'Google’s Agent Development Kit',
200
- disabled: true,
201
- },
202
196
  ];
203
197
  const TRANSPORTS = [
204
198
  {
@@ -2,6 +2,8 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Button, Flash, Heading, Label, Text, Textarea } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
+ import { AnonymousKeyExpired } from '@datalayer/core/lib/components/anonymous/AnonymousKeyExpired';
6
+ import { AnonymousKeyTimer } from '@datalayer/core/lib/components/anonymous/AnonymousKeyTimer';
5
7
  import { stepCountIs, ToolLoopAgent } from 'ai';
6
8
  import { Streamdown } from 'streamdown';
7
9
  import { ThemedProvider } from './utils/themedProvider';
@@ -9,6 +11,7 @@ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
9
11
  import { AssistantStage } from '../chat/assistant/AssistantStage';
10
12
  import { useBrowserInference } from '../hooks/useBrowserInference';
11
13
  import { createBrowserModel } from '../runtimes/browser/model';
14
+ import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
12
15
  import { a2aPeerTool, connectA2APeer, } from '../runtimes/browser/a2aPeer';
13
16
  import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
14
17
  import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
@@ -60,7 +63,17 @@ function AgentA2ATeam() {
60
63
  const accountingUrl = import.meta.env.VITE_A2A_ACCOUNTING_URL ||
61
64
  `${runtimeUrl}/api/v1/a2a/agents/${ACCOUNTING.id}`;
62
65
  const accountingKey = import.meta.env.VITE_A2A_ACCOUNTING_KEY || undefined;
63
- const { inference, needsSignIn } = useBrowserInference(true);
66
+ const { inference, needsSignIn, anonymous } = useBrowserInference(true);
67
+ // The temporary key's own clock, read out of it: when it ends and how long
68
+ // it was signed for, as core's countdown draws them.
69
+ const accountingKeyClock = useMemo(() => {
70
+ const exp = accountingKey ? readJwtClaims(accountingKey)?.exp : undefined;
71
+ return accountingKey && exp
72
+ ? { expiresAt: exp * 1000, grantedMs: tokenLifetimeMs(accountingKey) }
73
+ : undefined;
74
+ }, [accountingKey]);
75
+ const [accountingKeyEnded, setAccountingKeyEnded] = useState(() => accountingKeyClock !== undefined &&
76
+ accountingKeyClock.expiresAt <= Date.now());
64
77
  const [peer, setPeer] = useState(null);
65
78
  const [peerError, setPeerError] = useState(null);
66
79
  const [sales, setSales] = useState({
@@ -235,7 +248,11 @@ function AgentA2ATeam() {
235
248
  const accountingWhere = accountingKey
236
249
  ? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
237
250
  : `on a runtime, ${new URL(accountingUrl).host}`;
238
- return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsxs(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: [TEAM.emoji, " ", TEAM.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: TEAM.description }), needsSignIn && (_jsx(Flash, { variant: "warning", sx: { mb: 3 }, children: "Sales asks its model through the Datalayer inference service: sign in, or wait for a visitor's trial key." })), peerError && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: ["Accounting is not reachable at ", accountingUrl, ": ", peerError, " Serve it with", ' ', _jsx("code", { children: "python examples/sales-accounting-a2a/serve_accounting.py" }), ' ', "on the local server, or", ' ', _jsx("code", { children: "loop apps run accounting.yaml --cloud --a2a" }), " and set", ' ', _jsx("code", { children: "VITE_A2A_ACCOUNTING_URL" }), "."] })), _jsxs(Box, { sx: {
251
+ return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsxs(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: [TEAM.emoji, " ", TEAM.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: TEAM.description }), ((anonymous.status === 'active' && anonymous.expiresAt) ||
252
+ (accountingKeyClock && !accountingKeyEnded)) && (_jsxs(Box, { sx: { display: 'flex', gap: 4, flexWrap: 'wrap', mb: 3 }, children: [anonymous.status === 'active' && anonymous.expiresAt && (_jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${SALES.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() })), accountingKeyClock && !accountingKeyEnded && (_jsx(AnonymousKeyTimer, { expiresAt: accountingKeyClock.expiresAt, grantedMs: accountingKeyClock.grantedMs, label: `${ACCOUNTING.name}'s temporary key`, onExpire: () => setAccountingKeyEnded(true) }))] })), anonymous.status === 'expired' && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyExpired, { agentName: SALES.name,
253
+ // Sales runs in this page: what ended is its model's key, and
254
+ // signing in gives it the person's own.
255
+ onSignedIn: () => useAnonymousSessionStore.getState().clear() }) })), accountingKeyEnded && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: [ACCOUNTING.name, "\u2019s temporary key has ended: it no longer answers. Make a new one with", ' ', _jsx("code", { children: "examples/sales-accounting-a2a/make_temp_key.py" }), " and reload."] })), needsSignIn && (_jsx(Flash, { variant: "warning", sx: { mb: 3 }, children: "Sales asks its model through the Datalayer inference service: sign in, or wait for a visitor's trial key." })), peerError && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: ["Accounting is not reachable at ", accountingUrl, ": ", peerError, " Serve it with", ' ', _jsx("code", { children: "python examples/sales-accounting-a2a/serve_accounting.py" }), ' ', "on the local server, or", ' ', _jsx("code", { children: "loop apps run accounting.yaml --cloud --a2a" }), " and set", ' ', _jsx("code", { children: "VITE_A2A_ACCOUNTING_URL" }), "."] })), _jsxs(Box, { sx: {
239
256
  position: 'relative',
240
257
  height: STAGE_HEIGHT,
241
258
  border: '1px solid',
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * The characters offered are what the enabled plugins contribute to
7
7
  * `loop.assistant.character` (T-24): Datalayer's four, and an owl from a
8
- * small example plugin (`utils/owlCharacterPlugin`).
8
+ * small example plugin (`utils/owlCharacterPlugin`). Pixel, the test sprite,
9
+ * is read through the clippy.js reader, as a character a person brings is,
10
+ * and so are the characters clippy.js publishes, fetched when picked.
9
11
  */
10
12
  import React from 'react';
11
13
  declare const ChatAssistantExample: React.FC;