@datalayer/agent-runtimes 1.3.71 → 1.3.73
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.d.ts +16 -1
- package/lib/chat/ChatFloating.js +62 -17
- package/lib/chat/assistant/AssistantStage.d.ts +17 -2
- package/lib/chat/assistant/AssistantStage.js +3 -3
- package/lib/chat/assistant/BalloonParts.d.ts +55 -0
- package/lib/chat/assistant/BalloonParts.js +140 -0
- package/lib/chat/assistant/ConversationBalloon.d.ts +5 -0
- package/lib/chat/assistant/ConversationBalloon.js +5 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +23 -2
- package/lib/chat/assistant/SpeechBalloon.js +22 -8
- package/lib/chat/assistant/toolLine.d.ts +69 -0
- package/lib/chat/assistant/toolLine.js +118 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +12 -0
- package/lib/components/teams/A2ATeamGraph.js +35 -8
- package/lib/components/teams/NotebookPreview.d.ts +39 -0
- package/lib/components/teams/NotebookPreview.js +68 -0
- package/lib/components/teams/NotebookPreviewView.d.ts +21 -0
- package/lib/components/teams/NotebookPreviewView.js +42 -0
- package/lib/components/teams/TeamNotebook.js +16 -1
- package/lib/components/teams/a2aTeamFlow.d.ts +5 -0
- package/lib/components/teams/a2aTeamFlow.js +11 -0
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/useA2ATeam.d.ts +8 -0
- package/lib/components/teams/useA2ATeam.js +38 -3
- package/lib/examples/AgentA2ATeamExample.d.ts +8 -4
- package/lib/examples/AgentA2ATeamExample.js +2 -2
- package/lib/examples/ChatAssistantExample.js +4 -2
- package/lib/examples/ChatAssistantGalleryExample.d.ts +5 -1
- package/lib/examples/ChatAssistantGalleryExample.js +40 -10
- package/lib/examples/utils/AssistantGalleryGrid.d.ts +14 -11
- package/lib/examples/utils/AssistantGalleryGrid.js +42 -1
- package/lib/examples/utils/assistantGallery.d.ts +49 -1
- package/lib/examples/utils/assistantGallery.js +52 -1
- package/lib/loop/apps/appspec.js +6 -0
- package/lib/loop/apps/checks.js +2 -0
- package/lib/loop/embed/AppEmbed.js +3 -1
- package/lib/loop/plugins/assistant/index.d.ts +14 -1
- package/lib/loop/plugins/assistant/index.js +39 -13
- package/lib/specs/appspecSchema.js +18 -0
- package/lib/stories/loop/ReferenceScreens.d.ts +1 -1
- package/lib/stories/loop/ReferenceScreens.js +20 -3
- package/lib/types/agentspecs.d.ts +8 -0
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import React from 'react';
|
|
19
19
|
import type { ChatVoice } from '../voice';
|
|
20
|
+
import { type BalloonDisplay, type BalloonToolLine } from './assistant/toolLine';
|
|
20
21
|
import { type AssistantCharacter } from './assistant/characters';
|
|
21
22
|
import type { AssistantCharacterData } from './assistant/formats/types';
|
|
22
23
|
import { type AssistantSaying } from './assistant/state';
|
|
@@ -38,6 +39,8 @@ export type FloatingConversation = {
|
|
|
38
39
|
saying?: AssistantSaying;
|
|
39
40
|
/** Whether its newest item is the answer being written. */
|
|
40
41
|
answering: boolean;
|
|
42
|
+
/** The tool it is calling now, or just called: said in the balloon. */
|
|
43
|
+
tool?: BalloonToolLine;
|
|
41
44
|
};
|
|
42
45
|
/**
|
|
43
46
|
* ChatFloating props — extends ChatCommonProps with floating/popup-specific configuration.
|
|
@@ -111,6 +114,18 @@ export interface ChatFloatingProps extends ChatCommonProps {
|
|
|
111
114
|
* @default 'paperclip'
|
|
112
115
|
*/
|
|
113
116
|
assistantCharacter?: string | AssistantCharacter | AssistantCharacterData;
|
|
117
|
+
/**
|
|
118
|
+
* How the floating assistant's balloon shows the conversation (LOOP
|
|
119
|
+
* T-23; an Appspec's `interface.balloon`):
|
|
120
|
+
* - 'history': every message, scrolled, under a header that counts them,
|
|
121
|
+
* the composer last; closed, a peek of the newest words.
|
|
122
|
+
* - 'current': only what it says or does now — the answer being written,
|
|
123
|
+
* or the tool it calls — in one compact balloon, *Now* on it; open, that
|
|
124
|
+
* line and the composer, nothing to scroll.
|
|
125
|
+
* Either way a tool call is said in plain words: "Using list_invoices…".
|
|
126
|
+
* @default 'history'
|
|
127
|
+
*/
|
|
128
|
+
balloonDisplay?: BalloonDisplay;
|
|
114
129
|
/**
|
|
115
130
|
* The runtime the floating assistant asks typed decisions at — its
|
|
116
131
|
* `/api/v1/configure/inference/decisions`, as the `decide` tool asks — and
|
|
@@ -162,5 +177,5 @@ export interface ChatFloatingProps extends ChatCommonProps {
|
|
|
162
177
|
* ChatFloating component
|
|
163
178
|
* A floating chat window built on ChatBase
|
|
164
179
|
*/
|
|
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;
|
|
180
|
+
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, balloonDisplay, conversation, decisions, voice, }: ChatFloatingProps): React.JSX.Element;
|
|
166
181
|
export default ChatFloating;
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -35,7 +35,9 @@ import { decisionsAskerAt } from './assistant/decisions';
|
|
|
35
35
|
import { DecisionAsk } from './assistant/DecisionAsk';
|
|
36
36
|
import { ServerSpeaker } from '../voice/speaker';
|
|
37
37
|
import { useSpokenAnswers } from '../voice/useSpokenAnswers';
|
|
38
|
-
import { BalloonApprovalMessage, CONVERSATION_BALLOON_WIDTH, ConversationBalloonClose, balloonTailAt, conversationBalloonHeight, conversationBalloonSx, peekLine, } from './assistant/ConversationBalloon';
|
|
38
|
+
import { BalloonApprovalMessage, CONVERSATION_BALLOON_WIDTH, ConversationBalloonClose, balloonTailAt, conversationBalloonHeight, conversationBalloonSx, CURRENT_BALLOON_HEIGHT, peekLine, } from './assistant/ConversationBalloon';
|
|
39
|
+
import { ConversationBalloonHeader, CurrentBalloonBody, } from './assistant/BalloonParts';
|
|
40
|
+
import { DEFAULT_BALLOON_DISPLAY, conversationCount, newestToolLine, toolLineText, } from './assistant/toolLine';
|
|
39
41
|
import { SpeechBalloon } from './assistant/SpeechBalloon';
|
|
40
42
|
import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
|
|
41
43
|
import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, ASSISTANT_WORDS, } from './assistant/state';
|
|
@@ -68,7 +70,7 @@ const clamp = (value, max) => Math.max(8, Math.min(value, Math.max(8, max)));
|
|
|
68
70
|
* ChatFloating component
|
|
69
71
|
* A floating chat window built on ChatBase
|
|
70
72
|
*/
|
|
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, }) {
|
|
73
|
+
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, balloonDisplay = DEFAULT_BALLOON_DISPLAY, conversation, decisions, voice, }) {
|
|
72
74
|
// Store-based state
|
|
73
75
|
const storeIsOpen = useChatOpen();
|
|
74
76
|
const storeMessages = useChatMessages();
|
|
@@ -131,6 +133,9 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
131
133
|
pendingApproval: false,
|
|
132
134
|
});
|
|
133
135
|
const [ownAnswering, setAnswering] = useState(false);
|
|
136
|
+
// The tool it calls now, and how many messages the history holds (T-23).
|
|
137
|
+
const [ownTool, setOwnTool] = useState();
|
|
138
|
+
const [messageCount, setMessageCount] = useState(0);
|
|
134
139
|
/*
|
|
135
140
|
* Its voice (VOICE.md V1): the answers said by the speech service, as
|
|
136
141
|
* they are written (VO-20), the state following the sound (VO-22).
|
|
@@ -187,6 +192,8 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
187
192
|
const [ownSaying, setSaying] = useState();
|
|
188
193
|
const saying = conversation ? conversation.saying : ownSaying;
|
|
189
194
|
const answering = conversation ? conversation.answering : ownAnswering;
|
|
195
|
+
const toolLine = conversation ? conversation.tool : ownTool;
|
|
196
|
+
const showsCurrent = balloonDisplay === 'current';
|
|
190
197
|
const [heardId, setHeardId] = useState();
|
|
191
198
|
const [freshSaying, setFreshSaying] = useState(false);
|
|
192
199
|
useEffect(() => {
|
|
@@ -244,6 +251,14 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
244
251
|
const assistantDecide = useMemo(() => decisions?.serverUrl
|
|
245
252
|
? decisionsAskerAt(decisions.serverUrl, decisions.token)
|
|
246
253
|
: undefined, [decisions?.serverUrl, decisions?.token]);
|
|
254
|
+
// At work: a turn runs, or the host's conversation says it does.
|
|
255
|
+
const atWork = assistantState === 'thinking' ||
|
|
256
|
+
assistantState === 'working' ||
|
|
257
|
+
assistantState === 'speaking';
|
|
258
|
+
// The tool line, while its call runs or its turn goes on (T-23).
|
|
259
|
+
const toolSaid = toolLine && (toolLine.phase === 'running' || atWork) ? toolLine : undefined;
|
|
260
|
+
// Current: what is said now — thinking until the answer's words arrive.
|
|
261
|
+
const currentWords = atWork && !answering ? 'Thinking…' : saying?.text || undefined;
|
|
247
262
|
const assistantBalloon = assistantState === 'paused'
|
|
248
263
|
? { text: ASSISTANT_WORDS.paused }
|
|
249
264
|
: speakerState.sentence
|
|
@@ -255,20 +270,38 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
255
270
|
? { text: ASSISTANT_WORDS.approval, approval: balloonApproval }
|
|
256
271
|
: assistantState === 'waiting'
|
|
257
272
|
? { text: ASSISTANT_WORDS.waiting }
|
|
258
|
-
:
|
|
259
|
-
? {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
: toolSaid
|
|
274
|
+
? { text: toolLineText(toolSaid), tool: toolSaid, busy: atWork }
|
|
275
|
+
: showsCurrent && (unheard || atWork)
|
|
276
|
+
? // Current: the words as they are written, whole.
|
|
277
|
+
{
|
|
278
|
+
text: currentWords ?? description,
|
|
279
|
+
more: saying?.more,
|
|
280
|
+
speaking: answering && atWork,
|
|
281
|
+
busy: atWork,
|
|
282
|
+
onDismiss: saying
|
|
283
|
+
? () => {
|
|
284
|
+
setHeardId(saying.id);
|
|
285
|
+
setFreshSaying(false);
|
|
286
|
+
}
|
|
287
|
+
: undefined,
|
|
288
|
+
}
|
|
289
|
+
: unheard && saying
|
|
290
|
+
? {
|
|
291
|
+
// A peek: the first words; the rest is in the conversation.
|
|
292
|
+
...peekLine(saying.text),
|
|
293
|
+
onDismiss: () => {
|
|
294
|
+
setHeardId(saying.id);
|
|
295
|
+
setFreshSaying(false);
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
: { text: description };
|
|
268
299
|
const balloonInsists = !!speakerState.sentence ||
|
|
269
300
|
assistantState === 'greeting' ||
|
|
270
301
|
assistantState === 'waiting' ||
|
|
271
302
|
!!balloonApproval ||
|
|
303
|
+
!!toolSaid ||
|
|
304
|
+
(showsCurrent && atWork) ||
|
|
272
305
|
(unheard && (assistantState === 'speaking' || freshSaying));
|
|
273
306
|
const panelOnLoadingChange = panelProps?.onLoadingChange;
|
|
274
307
|
const panelOnDisplayItemsChange = panelProps?.onDisplayItemsChange;
|
|
@@ -283,6 +316,11 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
283
316
|
? previous
|
|
284
317
|
: tool);
|
|
285
318
|
setAnswering(newestIsAnswer(items));
|
|
319
|
+
const line = newestToolLine(items);
|
|
320
|
+
setOwnTool(previous => previous?.id === line?.id && previous?.phase === line?.phase
|
|
321
|
+
? previous
|
|
322
|
+
: line);
|
|
323
|
+
setMessageCount(conversationCount(items));
|
|
286
324
|
setVoiceItems(items);
|
|
287
325
|
const said = latestSaying(items);
|
|
288
326
|
setSaying(previous => previous?.id === said?.id && previous?.text === said?.text
|
|
@@ -499,9 +537,14 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
499
537
|
const viewportWidth = typeof window === 'undefined' ? 1280 : window.innerWidth;
|
|
500
538
|
const viewportHeight = typeof window === 'undefined' ? 800 : window.innerHeight;
|
|
501
539
|
const conversationHeight = conversationBalloonHeight(viewportHeight);
|
|
540
|
+
// Open, `current` is the line and the composer; `history`, 60% of the window.
|
|
541
|
+
const currentOpen = showsCurrent && !conversation;
|
|
542
|
+
const openBalloonHeight = currentOpen
|
|
543
|
+
? CURRENT_BALLOON_HEIGHT
|
|
544
|
+
: conversationHeight;
|
|
502
545
|
const balloonGeometry = () => {
|
|
503
546
|
const widthPx = CONVERSATION_BALLOON_WIDTH;
|
|
504
|
-
const heightPx =
|
|
547
|
+
const heightPx = openBalloonHeight;
|
|
505
548
|
if (stageDrag.position) {
|
|
506
549
|
// Toward the page's inside: above the character, below it, or beside
|
|
507
550
|
// it when the window is too short for either.
|
|
@@ -713,7 +756,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
713
756
|
const popupHeight = isMobile
|
|
714
757
|
? '100%'
|
|
715
758
|
: conversationBalloon
|
|
716
|
-
?
|
|
759
|
+
? openBalloonHeight
|
|
717
760
|
: height;
|
|
718
761
|
// Mobile full-screen styles
|
|
719
762
|
const mobileStyles = isMobile
|
|
@@ -902,7 +945,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
902
945
|
? 'canvas.subtle'
|
|
903
946
|
: 'canvas.default')
|
|
904
947
|
: {}),
|
|
905
|
-
}, "data-conversation-balloon": conversationBalloon ? '' : undefined, children: [conversationBalloon ? (_jsx(ConversationBalloonClose, { onClose: handleToggle })) : null, viewMode === 'floating-draggable' && !isMobile && (_jsx(Box, { onPointerDown: drag.onHandlePointerDown, "aria-label": "Move the chat", sx: {
|
|
948
|
+
}, "data-conversation-balloon": conversationBalloon ? '' : undefined, "data-balloon-display": conversationBalloon ? balloonDisplay : undefined, children: [conversationBalloon ? (_jsx(ConversationBalloonClose, { onClose: handleToggle })) : null, conversationBalloon && !currentOpen ? (_jsx(ConversationBalloonHeader, { count: messageCount })) : null, viewMode === 'floating-draggable' && !isMobile && (_jsx(Box, { onPointerDown: drag.onHandlePointerDown, "aria-label": "Move the chat", sx: {
|
|
906
949
|
display: 'flex',
|
|
907
950
|
alignItems: 'center',
|
|
908
951
|
justifyContent: 'center',
|
|
@@ -957,7 +1000,9 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
957
1000
|
...poweredByProps,
|
|
958
1001
|
}, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
|
|
959
1002
|
showAvatars: true,
|
|
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:
|
|
1003
|
+
}, 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: conversationBalloon && currentOpen ? (
|
|
1004
|
+
// Current: the one thing said or done now, over the composer.
|
|
1005
|
+
_jsx(Box, { sx: { px: 3, pt: 3, pb: 2, overflow: 'hidden' }, children: _jsx(CurrentBalloonBody, { text: toolSaid ? undefined : (currentWords ?? description), tool: toolSaid, busy: atWork, speaking: answering && atWork }) })) : (children) }))] }));
|
|
961
1006
|
/*
|
|
962
1007
|
* The floating popup's button. Words not yet heard make it blink for the
|
|
963
1008
|
* person's attention, until the chat is opened; hovered, they show in a
|
|
@@ -1005,7 +1050,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
1005
1050
|
left: px(stageDrag.position.left),
|
|
1006
1051
|
top: px(stageDrag.position.top),
|
|
1007
1052
|
}
|
|
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: {
|
|
1053
|
+
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, balloonDisplay: balloonDisplay, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef, mouthLevel: mouthLevel })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
|
|
1009
1054
|
...getPositionStyles(),
|
|
1010
1055
|
}, children: _jsxs(Box, { sx: {
|
|
1011
1056
|
position: 'relative',
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @module chat/assistant/AssistantStage
|
|
16
16
|
*/
|
|
17
|
-
import type { JSX, RefObject } from 'react';
|
|
17
|
+
import type { JSX, ReactNode, RefObject } from 'react';
|
|
18
18
|
import { type AssistantCharacter } from './characters';
|
|
19
19
|
import type { AssistantCharacterData } from './formats/types';
|
|
20
20
|
import type { DecisionAsker } from './decisions';
|
|
21
|
+
import type { BalloonDisplay, BalloonToolLine } from './toolLine';
|
|
21
22
|
import { type AssistantAway, type AssistantState, type BalloonApproval } from './state';
|
|
22
23
|
/**
|
|
23
24
|
* Where the balloon goes so that it stays inside the window (T-23): toward
|
|
@@ -79,7 +80,21 @@ export interface AssistantStageProps {
|
|
|
79
80
|
approval?: BalloonApproval;
|
|
80
81
|
/** Puts the peek away (a × beside its line); never an approval's. */
|
|
81
82
|
onDismiss?: () => void;
|
|
83
|
+
/** The tool being called: "Using list_invoices…" in place of the words. */
|
|
84
|
+
tool?: BalloonToolLine;
|
|
85
|
+
/** Words are being written (`current`): read out once all have arrived. */
|
|
86
|
+
speaking?: boolean;
|
|
87
|
+
/** The agent is at work (`current`): *Now* breathes. */
|
|
88
|
+
busy?: boolean;
|
|
89
|
+
/** What goes with the words (`current`): a notebook given, read-only. */
|
|
90
|
+
attachment?: ReactNode;
|
|
82
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* How the balloon shows the conversation (LOOP T-23): a peek of it
|
|
94
|
+
* (`history`, the default — open, the conversation is the whole history),
|
|
95
|
+
* or the one thing being said or done now (`current`).
|
|
96
|
+
*/
|
|
97
|
+
balloonDisplay?: BalloonDisplay;
|
|
83
98
|
/** Show the balloon without being hovered: something new to say. */
|
|
84
99
|
insist?: boolean;
|
|
85
100
|
/** Send it away (T-27): for the page, for the session, or for good. */
|
|
@@ -114,5 +129,5 @@ export declare function mouthOpening(level: number): 0 | 1 | 2 | 3;
|
|
|
114
129
|
* `data-assistant-mouth`, each frame, without drawing the character again.
|
|
115
130
|
*/
|
|
116
131
|
export declare function useMouth(stageRef: RefObject<HTMLElement | null>, mouthLevel: (() => number) | undefined, speaking: boolean): void;
|
|
117
|
-
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, mouthLevel, stayPut, }: AssistantStageProps): JSX.Element;
|
|
132
|
+
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, mouthLevel, stayPut, balloonDisplay, }: AssistantStageProps): JSX.Element;
|
|
118
133
|
export default AssistantStage;
|
|
@@ -209,7 +209,7 @@ export function useMouth(stageRef, mouthLevel, speaking) {
|
|
|
209
209
|
};
|
|
210
210
|
}, [stageRef, mouthLevel, speaking]);
|
|
211
211
|
}
|
|
212
|
-
export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, mouthLevel, stayPut = false, }) {
|
|
212
|
+
export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, mouthLevel, stayPut = false, balloonDisplay = 'history', }) {
|
|
213
213
|
// A shipped one by id, a drawing contributed by a plugin (T-24), or a
|
|
214
214
|
// character read from a file (T-26).
|
|
215
215
|
const shipped = typeof character === 'string'
|
|
@@ -232,7 +232,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
232
232
|
// A decision being asked, or its answer, keeps the balloon up.
|
|
233
233
|
const [deciding, setDeciding] = useState(false);
|
|
234
234
|
const showBalloon = !aside && !open && !!balloon && (hovered || insist || deciding);
|
|
235
|
-
return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, "data-assistant-aside": aside, sx: {
|
|
235
|
+
return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, "data-assistant-aside": aside, "data-assistant-balloon": balloonDisplay, sx: {
|
|
236
236
|
position: 'fixed',
|
|
237
237
|
zIndex: 1002,
|
|
238
238
|
...place,
|
|
@@ -331,7 +331,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
331
331
|
'@media (prefers-reduced-motion: reduce)': {
|
|
332
332
|
'& *': { animation: 'none !important' },
|
|
333
333
|
},
|
|
334
|
-
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onDismissPeek: balloon.onDismiss, decide: decide, onDecisionActive: setDeciding, wide: deciding, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
|
|
334
|
+
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onDismissPeek: balloon.onDismiss, display: balloonDisplay, tool: balloon.tool, speaking: balloon.speaking, busy: balloon.busy, attachment: balloon.attachment, decide: decide, onDecisionActive: setDeciding, wide: deciding, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
|
|
335
335
|
pressedAt.current = { x: event.clientX, y: event.clientY };
|
|
336
336
|
onDragStart(event);
|
|
337
337
|
}, onClick: (event) => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts the two balloon displays are drawn with (LOOP T-23): the tool
|
|
3
|
+
* line ("Using **list_invoices**…"), what a screen reader hears of it, the
|
|
4
|
+
* `current` balloon's *Now*, and the `history` balloon's header
|
|
5
|
+
* ("Conversation · 6").
|
|
6
|
+
*
|
|
7
|
+
* @module chat/assistant/BalloonParts
|
|
8
|
+
*/
|
|
9
|
+
import type { JSX, ReactNode } from 'react';
|
|
10
|
+
import { type BalloonToolLine } from './toolLine';
|
|
11
|
+
/**
|
|
12
|
+
* A tool call in the balloon, in plain words: its mark when the catalogue
|
|
13
|
+
* gives one (an MCP server's, a skill's, a tool set's), else a tool; "Using
|
|
14
|
+
* **list_invoices**…", "Done: **list_invoices**", "**list_invoices** failed".
|
|
15
|
+
* Seen, not read out: {@link ToolLineAnnouncer} says it once per change.
|
|
16
|
+
*/
|
|
17
|
+
export declare function BalloonToolLineView({ line, }: {
|
|
18
|
+
line: BalloonToolLine;
|
|
19
|
+
}): JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* What a screen reader hears of the tool lines, politely: once when a call
|
|
22
|
+
* starts and once when it ends, never while it runs nor while words stream.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ToolLineAnnouncer({ line, }: {
|
|
25
|
+
line?: BalloonToolLine;
|
|
26
|
+
}): JSX.Element;
|
|
27
|
+
/**
|
|
28
|
+
* The `current` balloon's mark: *Now*, with a dot that breathes while the
|
|
29
|
+
* agent is at work — still for a reader who asks for reduced motion.
|
|
30
|
+
*/
|
|
31
|
+
export declare function BalloonNow({ busy }: {
|
|
32
|
+
busy?: boolean;
|
|
33
|
+
}): JSX.Element;
|
|
34
|
+
/** How the `history` balloon names itself: "Conversation · 6". */
|
|
35
|
+
export declare function conversationHeaderText(count: number): string;
|
|
36
|
+
/**
|
|
37
|
+
* The `history` balloon's header: what it holds — the conversation, and how
|
|
38
|
+
* many messages — over the scrolled history and the composer.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ConversationBalloonHeader({ count, }: {
|
|
41
|
+
count: number;
|
|
42
|
+
}): JSX.Element;
|
|
43
|
+
/**
|
|
44
|
+
* What the `current` balloon holds: *Now*, then the one thing being said or
|
|
45
|
+
* done — the tool line while a tool runs, else the words — cut to a few
|
|
46
|
+
* lines, never scrolled; and what goes with them (`attachment`).
|
|
47
|
+
*/
|
|
48
|
+
export declare function CurrentBalloonBody({ text, tool, busy, speaking, attachment, }: {
|
|
49
|
+
text?: string;
|
|
50
|
+
tool?: BalloonToolLine;
|
|
51
|
+
busy?: boolean;
|
|
52
|
+
/** Words are arriving: read out once they have. */
|
|
53
|
+
speaking?: boolean;
|
|
54
|
+
attachment?: ReactNode;
|
|
55
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { CheckIcon, ToolsIcon, XIcon } from '@primer/octicons-react';
|
|
6
|
+
import { SpecMark, hasMark, marksOfToolCall } from '../marks';
|
|
7
|
+
import { toolAnnouncement, toolLineParts, } from './toolLine';
|
|
8
|
+
/** Out of sight, still heard. */
|
|
9
|
+
const VISUALLY_HIDDEN = {
|
|
10
|
+
position: 'absolute',
|
|
11
|
+
width: 1,
|
|
12
|
+
height: 1,
|
|
13
|
+
overflow: 'hidden',
|
|
14
|
+
clip: 'rect(0 0 0 0)',
|
|
15
|
+
whiteSpace: 'nowrap',
|
|
16
|
+
};
|
|
17
|
+
const PHASE_COLOR = {
|
|
18
|
+
running: 'fg.muted',
|
|
19
|
+
done: 'success.fg',
|
|
20
|
+
failed: 'danger.fg',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A tool call in the balloon, in plain words: its mark when the catalogue
|
|
24
|
+
* gives one (an MCP server's, a skill's, a tool set's), else a tool; "Using
|
|
25
|
+
* **list_invoices**…", "Done: **list_invoices**", "**list_invoices** failed".
|
|
26
|
+
* Seen, not read out: {@link ToolLineAnnouncer} says it once per change.
|
|
27
|
+
*/
|
|
28
|
+
export function BalloonToolLineView({ line, }) {
|
|
29
|
+
const { before, name, after } = toolLineParts(line);
|
|
30
|
+
const marks = line.tool ? marksOfToolCall(line.tool, undefined) : null;
|
|
31
|
+
const Icon = line.phase === 'done'
|
|
32
|
+
? CheckIcon
|
|
33
|
+
: line.phase === 'failed'
|
|
34
|
+
? XIcon
|
|
35
|
+
: ToolsIcon;
|
|
36
|
+
return (_jsxs(Box, { as: "span", "aria-hidden": "true", "data-balloon-tool": line.phase, "data-balloon-tool-name": line.tool || undefined, sx: {
|
|
37
|
+
display: 'inline-flex',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
gap: 1,
|
|
40
|
+
minWidth: 0,
|
|
41
|
+
maxWidth: '100%',
|
|
42
|
+
color: line.phase === 'failed' ? 'danger.fg' : 'fg.default',
|
|
43
|
+
}, children: [_jsx(Box, { as: "span", sx: {
|
|
44
|
+
display: 'inline-flex',
|
|
45
|
+
flexShrink: 0,
|
|
46
|
+
color: PHASE_COLOR[line.phase],
|
|
47
|
+
}, children: line.phase === 'running' && hasMark(marks) ? (_jsx(SpecMark, { icon: marks?.icon, emoji: marks?.emoji, size: 14 })) : (_jsx(Icon, { size: 14 })) }), _jsxs(Box, { as: "span", sx: {
|
|
48
|
+
minWidth: 0,
|
|
49
|
+
overflow: 'hidden',
|
|
50
|
+
textOverflow: 'ellipsis',
|
|
51
|
+
whiteSpace: 'nowrap',
|
|
52
|
+
}, children: [before, name ? (_jsx(Box, { as: "strong", sx: { fontWeight: 'semibold' }, children: name })) : null, after] })] }));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* What a screen reader hears of the tool lines, politely: once when a call
|
|
56
|
+
* starts and once when it ends, never while it runs nor while words stream.
|
|
57
|
+
*/
|
|
58
|
+
export function ToolLineAnnouncer({ line, }) {
|
|
59
|
+
const last = useRef(undefined);
|
|
60
|
+
const [said, setSaid] = useState('');
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const words = toolAnnouncement(line, last.current);
|
|
63
|
+
if (line && words) {
|
|
64
|
+
last.current = { id: line.id, phase: line.phase };
|
|
65
|
+
setSaid(words);
|
|
66
|
+
}
|
|
67
|
+
}, [line?.id, line?.phase]);
|
|
68
|
+
return (_jsx(Box, { as: "span", role: "status", "aria-live": "polite", "data-balloon-announce": "", sx: VISUALLY_HIDDEN, children: said }));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The `current` balloon's mark: *Now*, with a dot that breathes while the
|
|
72
|
+
* agent is at work — still for a reader who asks for reduced motion.
|
|
73
|
+
*/
|
|
74
|
+
export function BalloonNow({ busy = false }) {
|
|
75
|
+
return (_jsxs(Box, { as: "span", "data-balloon-now": busy ? 'busy' : '', sx: {
|
|
76
|
+
display: 'inline-flex',
|
|
77
|
+
alignItems: 'center',
|
|
78
|
+
gap: '6px',
|
|
79
|
+
fontSize: 0,
|
|
80
|
+
fontWeight: 'semibold',
|
|
81
|
+
color: 'accent.fg',
|
|
82
|
+
textTransform: 'uppercase',
|
|
83
|
+
letterSpacing: '0.04em',
|
|
84
|
+
'@keyframes balloonNowBreathe': {
|
|
85
|
+
'0%, 100%': { opacity: 1, transform: 'scale(1)' },
|
|
86
|
+
'50%': { opacity: 0.35, transform: 'scale(0.7)' },
|
|
87
|
+
},
|
|
88
|
+
'& [data-balloon-now-dot]': {
|
|
89
|
+
width: 6,
|
|
90
|
+
height: 6,
|
|
91
|
+
borderRadius: '50%',
|
|
92
|
+
bg: 'accent.emphasis',
|
|
93
|
+
animation: busy
|
|
94
|
+
? 'balloonNowBreathe 1.2s ease-in-out infinite'
|
|
95
|
+
: 'none',
|
|
96
|
+
},
|
|
97
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
98
|
+
'& [data-balloon-now-dot]': { animation: 'none' },
|
|
99
|
+
},
|
|
100
|
+
}, children: [_jsx(Box, { as: "span", "data-balloon-now-dot": "", "aria-hidden": "true" }), "Now"] }));
|
|
101
|
+
}
|
|
102
|
+
/** How the `history` balloon names itself: "Conversation · 6". */
|
|
103
|
+
export function conversationHeaderText(count) {
|
|
104
|
+
return count > 0 ? `Conversation · ${count}` : 'Conversation';
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The `history` balloon's header: what it holds — the conversation, and how
|
|
108
|
+
* many messages — over the scrolled history and the composer.
|
|
109
|
+
*/
|
|
110
|
+
export function ConversationBalloonHeader({ count, }) {
|
|
111
|
+
return (_jsx(Box, { "data-balloon-header": "", sx: {
|
|
112
|
+
flexShrink: 0,
|
|
113
|
+
display: 'flex',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: 2,
|
|
116
|
+
// Room for the close control over the corner.
|
|
117
|
+
pl: 3,
|
|
118
|
+
pr: 6,
|
|
119
|
+
py: 2,
|
|
120
|
+
borderBottom: '1px solid',
|
|
121
|
+
borderColor: 'border.muted',
|
|
122
|
+
bg: 'canvas.default',
|
|
123
|
+
borderTopLeftRadius: 'var(--theme-radius-bubble, 16px)',
|
|
124
|
+
borderTopRightRadius: 'var(--theme-radius-bubble, 16px)',
|
|
125
|
+
}, children: _jsx(Text, { sx: { fontSize: 0, fontWeight: 'semibold', color: 'fg.muted' }, children: conversationHeaderText(count) }) }));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* What the `current` balloon holds: *Now*, then the one thing being said or
|
|
129
|
+
* done — the tool line while a tool runs, else the words — cut to a few
|
|
130
|
+
* lines, never scrolled; and what goes with them (`attachment`).
|
|
131
|
+
*/
|
|
132
|
+
export function CurrentBalloonBody({ text, tool, busy = false, speaking = false, attachment, }) {
|
|
133
|
+
return (_jsxs(Box, { "data-balloon-current": "", sx: { display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }, children: [_jsx(BalloonNow, { busy: busy }), tool ? (_jsx(BalloonToolLineView, { line: tool })) : text ? (_jsx(Box, { "aria-live": "polite", "aria-busy": speaking, "data-balloon-current-text": "", sx: {
|
|
134
|
+
display: '-webkit-box',
|
|
135
|
+
WebkitLineClamp: 4,
|
|
136
|
+
WebkitBoxOrient: 'vertical',
|
|
137
|
+
overflow: 'hidden',
|
|
138
|
+
overflowWrap: 'anywhere',
|
|
139
|
+
}, children: text })) : null, attachment, _jsx(ToolLineAnnouncer, { line: tool })] }));
|
|
140
|
+
}
|
|
@@ -18,6 +18,11 @@ export declare const CONVERSATION_BALLOON_WIDTH = 380;
|
|
|
18
18
|
/** The open balloon's tallest, in pixels, and its share of the window. */
|
|
19
19
|
export declare const CONVERSATION_BALLOON_MAX_HEIGHT = 560;
|
|
20
20
|
export declare const CONVERSATION_BALLOON_VIEWPORT_SHARE = 0.6;
|
|
21
|
+
/**
|
|
22
|
+
* The open balloon's height when it shows only what is said now
|
|
23
|
+
* (`current`): the line and the composer, nothing to scroll.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CURRENT_BALLOON_HEIGHT = 230;
|
|
21
26
|
/** The open balloon's height for a window this tall: 60% of it, 560px at most. */
|
|
22
27
|
export declare function conversationBalloonHeight(viewportHeight: number): number;
|
|
23
28
|
/**
|
|
@@ -9,6 +9,11 @@ export const CONVERSATION_BALLOON_WIDTH = 380;
|
|
|
9
9
|
/** The open balloon's tallest, in pixels, and its share of the window. */
|
|
10
10
|
export const CONVERSATION_BALLOON_MAX_HEIGHT = 560;
|
|
11
11
|
export const CONVERSATION_BALLOON_VIEWPORT_SHARE = 0.6;
|
|
12
|
+
/**
|
|
13
|
+
* The open balloon's height when it shows only what is said now
|
|
14
|
+
* (`current`): the line and the composer, nothing to scroll.
|
|
15
|
+
*/
|
|
16
|
+
export const CURRENT_BALLOON_HEIGHT = 230;
|
|
12
17
|
/** The open balloon's height for a window this tall: 60% of it, 560px at most. */
|
|
13
18
|
export function conversationBalloonHeight(viewportHeight) {
|
|
14
19
|
return Math.max(240, Math.min(CONVERSATION_BALLOON_MAX_HEIGHT, Math.round(viewportHeight * CONVERSATION_BALLOON_VIEWPORT_SHARE)));
|
|
@@ -9,11 +9,19 @@
|
|
|
9
9
|
* the conversation is elsewhere (the LOOP workspace), *Ask a decision*: a
|
|
10
10
|
* typed decision asked of Jev, its answer said back in the balloon.
|
|
11
11
|
*
|
|
12
|
+
* Shown `current` (LOOP T-23), it is the one thing being said or done now:
|
|
13
|
+
* *Now*, then the words as they are written, or the tool being called
|
|
14
|
+
* ("Using **list_invoices**…"), cut to a few lines, and what goes with them
|
|
15
|
+
* (a notebook given, read-only). Either way a tool line stands in for the
|
|
16
|
+
* peek while a tool runs, and a screen reader hears it once as it starts and
|
|
17
|
+
* once as it ends.
|
|
18
|
+
*
|
|
12
19
|
* @module chat/assistant/SpeechBalloon
|
|
13
20
|
*/
|
|
14
|
-
import type { JSX } from 'react';
|
|
21
|
+
import type { JSX, ReactNode } from 'react';
|
|
15
22
|
import { type BalloonApproval } from './state';
|
|
16
23
|
import type { DecisionAsker } from './decisions';
|
|
24
|
+
import type { BalloonDisplay, BalloonToolLine } from './toolLine';
|
|
17
25
|
export interface SpeechBalloonProps {
|
|
18
26
|
text: string;
|
|
19
27
|
/** There is more than the balloon holds. */
|
|
@@ -38,6 +46,19 @@ export interface SpeechBalloonProps {
|
|
|
38
46
|
wide?: boolean;
|
|
39
47
|
/** Puts the peek away: a × beside its line. Never for an approval. */
|
|
40
48
|
onDismissPeek?: () => void;
|
|
49
|
+
/**
|
|
50
|
+
* How it is shown: a peek of the conversation (`history`, the default),
|
|
51
|
+
* or the one thing being said or done now (`current`).
|
|
52
|
+
*/
|
|
53
|
+
display?: BalloonDisplay;
|
|
54
|
+
/** The tool being called: said in place of the words while it runs. */
|
|
55
|
+
tool?: BalloonToolLine;
|
|
56
|
+
/** Words are being written: read out once they have all arrived. */
|
|
57
|
+
speaking?: boolean;
|
|
58
|
+
/** The agent is at work: *Now* breathes. */
|
|
59
|
+
busy?: boolean;
|
|
60
|
+
/** What goes with the words, under them: a notebook given, read-only. */
|
|
61
|
+
attachment?: ReactNode;
|
|
41
62
|
}
|
|
42
|
-
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, onDismissPeek, }: SpeechBalloonProps): JSX.Element;
|
|
63
|
+
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, onDismissPeek, display, tool, speaking, busy, attachment, }: SpeechBalloonProps): JSX.Element;
|
|
43
64
|
export default SpeechBalloon;
|
|
@@ -5,8 +5,10 @@ import { XIcon } from '@primer/octicons-react';
|
|
|
5
5
|
import { ASSISTANT_WORDS } from './state';
|
|
6
6
|
import { moreWaiting } from './ConversationBalloon';
|
|
7
7
|
import { DecisionAsk } from './DecisionAsk';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { BalloonToolLineView, CurrentBalloonBody, ToolLineAnnouncer, } from './BalloonParts';
|
|
9
|
+
export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, onDismissPeek, display = 'history', tool, speaking = false, busy = false, attachment, }) {
|
|
10
|
+
const current = display === 'current';
|
|
11
|
+
return (_jsxs(Box, { "data-speech-balloon": "", "data-balloon-display": display, sx: {
|
|
10
12
|
position: 'absolute',
|
|
11
13
|
// Pixels, as strings: a number here is read as the theme's space
|
|
12
14
|
// scale (`-7` would be `-space[7]`, 48px).
|
|
@@ -14,14 +16,15 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
14
16
|
? { bottom: `${above}px` }
|
|
15
17
|
: { top: `${above}px` }),
|
|
16
18
|
[align]: 0,
|
|
17
|
-
maxWidth: wide ? 300 : 280,
|
|
18
|
-
width: wide ? 300 : 'max-content',
|
|
19
|
+
maxWidth: wide || (current && attachment) ? 300 : current ? 260 : 280,
|
|
20
|
+
width: wide || (current && attachment) ? 300 : 'max-content',
|
|
19
21
|
px: 3,
|
|
20
22
|
py: 2,
|
|
21
23
|
bg: 'canvas.default',
|
|
22
24
|
color: 'fg.default',
|
|
23
25
|
border: '1px solid',
|
|
24
|
-
|
|
26
|
+
// The current balloon is the agent's voice now: its accent's edge.
|
|
27
|
+
borderColor: current ? 'accent.muted' : 'border.default',
|
|
25
28
|
borderRadius: 'var(--theme-radius-bubble, 16px)',
|
|
26
29
|
boxShadow: 'shadow.medium',
|
|
27
30
|
fontSize: 1,
|
|
@@ -37,10 +40,21 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
37
40
|
bg: 'canvas.default',
|
|
38
41
|
borderRight: '1px solid',
|
|
39
42
|
borderBottom: '1px solid',
|
|
40
|
-
borderColor: 'border.default',
|
|
43
|
+
borderColor: current ? 'accent.muted' : 'border.default',
|
|
41
44
|
transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
|
|
42
45
|
},
|
|
43
|
-
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', gap: 1 }, children: [_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "",
|
|
46
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', gap: 1 }, children: [current && !approval ? (_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", onClick: onOpen, sx: {
|
|
47
|
+
flex: 1,
|
|
48
|
+
minWidth: 0,
|
|
49
|
+
m: 0,
|
|
50
|
+
p: 0,
|
|
51
|
+
border: 0,
|
|
52
|
+
bg: 'transparent',
|
|
53
|
+
color: 'inherit',
|
|
54
|
+
font: 'inherit',
|
|
55
|
+
textAlign: 'left',
|
|
56
|
+
cursor: 'pointer',
|
|
57
|
+
}, children: _jsx(CurrentBalloonBody, { text: text, tool: tool, busy: busy, speaking: speaking }) })) : (_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", "data-balloon-more": more ? '' : undefined, onClick: onOpen, sx: {
|
|
44
58
|
flex: 1,
|
|
45
59
|
minWidth: 0,
|
|
46
60
|
m: 0,
|
|
@@ -52,6 +66,6 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
52
66
|
textAlign: 'left',
|
|
53
67
|
cursor: 'pointer',
|
|
54
68
|
'&:hover': { textDecoration: 'underline' },
|
|
55
|
-
}, children: text }), onDismissPeek && !approval ? (_jsx(IconButton, { icon: XIcon, size: "small", variant: "invisible", "aria-label": "Dismiss", "data-balloon-dismiss": "", onClick: onDismissPeek, sx: { mt: '-4px', mr: '-8px', flexShrink: 0 } })) : null] }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: moreWaiting(approval.others) })) : null] })), decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive })] }));
|
|
69
|
+
}, children: tool ? (_jsx(BalloonToolLineView, { line: tool })) : (_jsx(Box, { as: "span", role: "status", "aria-live": "polite", children: text })) })), onDismissPeek && !approval ? (_jsx(IconButton, { icon: XIcon, size: "small", variant: "invisible", "aria-label": "Dismiss", "data-balloon-dismiss": "", onClick: onDismissPeek, sx: { mt: '-4px', mr: '-8px', flexShrink: 0 } })) : null] }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: moreWaiting(approval.others) })) : null] })), current && attachment ? (_jsx(Box, { "data-balloon-attachment": "", sx: { mt: 2 }, children: attachment })) : null, !current ? _jsx(ToolLineAnnouncer, { line: tool }) : null, decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive })] }));
|
|
56
70
|
}
|
|
57
71
|
export default SpeechBalloon;
|