@datalayer/agent-runtimes 1.3.60 → 1.3.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/chat/ChatFloating.js +138 -23
- package/lib/chat/assistant/AssistantStage.d.ts +6 -4
- package/lib/chat/assistant/AssistantStage.js +1 -1
- package/lib/chat/assistant/ConversationBalloon.d.ts +71 -0
- package/lib/chat/assistant/ConversationBalloon.js +102 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +10 -6
- package/lib/chat/assistant/SpeechBalloon.js +17 -12
- package/lib/chat/assistant/state.d.ts +1 -1
- package/lib/chat/assistant/state.js +1 -1
- package/lib/chat/base/ChatBase.js +22 -4
- package/lib/examples/ChatAssistantExample.js +12 -1
- package/lib/loop/plugins/assistant/index.js +9 -1
- package/lib/stories/loop/ReferenceScreens.d.ts +6 -2
- package/lib/stories/loop/ReferenceScreens.js +63 -6
- package/lib/types/chat.d.ts +12 -0
- package/package.json +1 -1
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -32,6 +32,8 @@ import { useViewportDrag } from './useViewportDrag';
|
|
|
32
32
|
import { disabledChatViewModes, resolveMountPoint } from './viewModes';
|
|
33
33
|
import { AssistantStage } from './assistant/AssistantStage';
|
|
34
34
|
import { decisionsAskerAt } from './assistant/decisions';
|
|
35
|
+
import { DecisionAsk } from './assistant/DecisionAsk';
|
|
36
|
+
import { BalloonApprovalMessage, CONVERSATION_BALLOON_WIDTH, ConversationBalloonClose, balloonTailAt, conversationBalloonHeight, conversationBalloonSx, peekLine, } from './assistant/ConversationBalloon';
|
|
35
37
|
import { SpeechBalloon } from './assistant/SpeechBalloon';
|
|
36
38
|
import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
|
|
37
39
|
import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, ASSISTANT_WORDS, } from './assistant/state';
|
|
@@ -205,7 +207,14 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
205
207
|
: assistantState === 'waiting'
|
|
206
208
|
? { text: ASSISTANT_WORDS.waiting }
|
|
207
209
|
: unheard && saying
|
|
208
|
-
? {
|
|
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
|
+
}
|
|
209
218
|
: { text: description };
|
|
210
219
|
const balloonInsists = assistantState === 'greeting' ||
|
|
211
220
|
assistantState === 'waiting' ||
|
|
@@ -433,19 +442,23 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
433
442
|
}, [isOpen, handleToggle]);
|
|
434
443
|
/*
|
|
435
444
|
* The conversation as the assistant's balloon: above the character, its
|
|
436
|
-
*
|
|
445
|
+
* edge on the character's, never off the screen, and its tail toward the
|
|
446
|
+
* character — none when it stands beside it.
|
|
437
447
|
*/
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
448
|
+
const viewportWidth = typeof window === 'undefined' ? 1280 : window.innerWidth;
|
|
449
|
+
const viewportHeight = typeof window === 'undefined' ? 800 : window.innerHeight;
|
|
450
|
+
const conversationHeight = conversationBalloonHeight(viewportHeight);
|
|
451
|
+
const balloonGeometry = () => {
|
|
452
|
+
const widthPx = CONVERSATION_BALLOON_WIDTH;
|
|
453
|
+
const heightPx = conversationHeight;
|
|
441
454
|
if (stageDrag.position) {
|
|
442
455
|
// Toward the page's inside: above the character, below it, or beside
|
|
443
456
|
// it when the window is too short for either.
|
|
444
457
|
const { left, top } = stageDrag.position;
|
|
445
|
-
const onLeft = left + ASSISTANT_SIZE / 2 <
|
|
458
|
+
const onLeft = left + ASSISTANT_SIZE / 2 < viewportWidth / 2;
|
|
446
459
|
const above = top - heightPx - 16;
|
|
447
460
|
const below = top + ASSISTANT_SIZE + 16;
|
|
448
|
-
const beside = above < 8 && below + heightPx >
|
|
461
|
+
const beside = above < 8 && below + heightPx > viewportHeight - 8;
|
|
449
462
|
const x = beside
|
|
450
463
|
? onLeft
|
|
451
464
|
? left + ASSISTANT_SIZE + 16
|
|
@@ -458,17 +471,38 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
458
471
|
: above >= 8
|
|
459
472
|
? above
|
|
460
473
|
: below;
|
|
474
|
+
const placedX = clamp(x, viewportWidth - widthPx - 8);
|
|
461
475
|
return {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
476
|
+
place: {
|
|
477
|
+
left: px(placedX),
|
|
478
|
+
top: px(clamp(y, viewportHeight - heightPx - 8)),
|
|
479
|
+
right: 'auto',
|
|
480
|
+
bottom: 'auto',
|
|
481
|
+
},
|
|
482
|
+
tail: beside
|
|
483
|
+
? { edge: 'none' }
|
|
484
|
+
: {
|
|
485
|
+
edge: above >= 8 ? 'bottom' : 'top',
|
|
486
|
+
at: balloonTailAt(left + ASSISTANT_SIZE / 2, placedX),
|
|
487
|
+
},
|
|
466
488
|
};
|
|
467
489
|
}
|
|
490
|
+
const onRight = position.endsWith('right');
|
|
491
|
+
const characterMiddle = onRight
|
|
492
|
+
? viewportWidth - offset - ASSISTANT_SIZE / 2
|
|
493
|
+
: offset + ASSISTANT_SIZE / 2;
|
|
494
|
+
const balloonLeft = onRight ? viewportWidth - offset - widthPx : offset;
|
|
495
|
+
const tail = {
|
|
496
|
+
edge: position.startsWith('bottom') ? 'bottom' : 'top',
|
|
497
|
+
at: balloonTailAt(characterMiddle, balloonLeft),
|
|
498
|
+
};
|
|
468
499
|
const corner = getPositionStyles();
|
|
469
|
-
return
|
|
470
|
-
|
|
471
|
-
|
|
500
|
+
return {
|
|
501
|
+
place: position.startsWith('bottom')
|
|
502
|
+
? { ...corner, bottom: px(offset + ASSISTANT_SIZE + 16) }
|
|
503
|
+
: { ...corner, top: px(offset + ASSISTANT_SIZE + 16) },
|
|
504
|
+
tail,
|
|
505
|
+
};
|
|
472
506
|
};
|
|
473
507
|
// Handle new chat
|
|
474
508
|
const handleNewChat = useCallback(() => {
|
|
@@ -520,6 +554,20 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
520
554
|
}, animationDuration);
|
|
521
555
|
return () => clearTimeout(timer);
|
|
522
556
|
}, [hostsConversation, isOpen, animationDuration, focusTrigger]);
|
|
557
|
+
/*
|
|
558
|
+
* The assistant's balloon opens on the newest message: the history may
|
|
559
|
+
* have grown — or arrived — while it was closed.
|
|
560
|
+
*/
|
|
561
|
+
useEffect(() => {
|
|
562
|
+
if (!assistantShown || !isOpen) {
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
const timer = setTimeout(() => {
|
|
566
|
+
const history = popupRef.current?.querySelector('[data-chat-history]');
|
|
567
|
+
history?.scrollTo?.({ top: history.scrollHeight });
|
|
568
|
+
}, animationDuration);
|
|
569
|
+
return () => clearTimeout(timer);
|
|
570
|
+
}, [assistantShown, isOpen, animationDuration]);
|
|
523
571
|
// Click outside to close
|
|
524
572
|
useEffect(() => {
|
|
525
573
|
if (!clickOutsideToClose || !isOpen)
|
|
@@ -604,8 +652,18 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
604
652
|
})
|
|
605
653
|
: undefined;
|
|
606
654
|
// Responsive dimensions
|
|
607
|
-
const
|
|
608
|
-
|
|
655
|
+
const conversationBalloon = assistantShown ? balloonGeometry() : undefined;
|
|
656
|
+
// The assistant's conversation is a balloon of its own size (T-23).
|
|
657
|
+
const popupWidth = isMobile
|
|
658
|
+
? '100%'
|
|
659
|
+
: conversationBalloon
|
|
660
|
+
? CONVERSATION_BALLOON_WIDTH
|
|
661
|
+
: width;
|
|
662
|
+
const popupHeight = isMobile
|
|
663
|
+
? '100%'
|
|
664
|
+
: conversationBalloon
|
|
665
|
+
? conversationHeight
|
|
666
|
+
: height;
|
|
609
667
|
// Mobile full-screen styles
|
|
610
668
|
const mobileStyles = isMobile
|
|
611
669
|
? {
|
|
@@ -621,6 +679,44 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
621
679
|
: {};
|
|
622
680
|
// Close button for header
|
|
623
681
|
const closeButton = (_jsx(Tooltip, { text: `Close${escapeToClose ? ' (Esc)' : ''}`, direction: "s", children: _jsx(IconButton, { icon: XIcon, "aria-label": "Close", onClick: handleToggle, variant: "invisible", size: "small" }) }));
|
|
682
|
+
/*
|
|
683
|
+
* The chat as the assistant's balloon holds it (T-23): the history and the
|
|
684
|
+
* composer, its Lexical variant, and nothing else — no header, no footer,
|
|
685
|
+
* none of the session's controls; the welcome as the first message, an
|
|
686
|
+
* approval as a message with Approve and Deny, and *Ask a decision* beside
|
|
687
|
+
* the composer when there is a runtime to ask.
|
|
688
|
+
*/
|
|
689
|
+
const assistantChat = conversationBalloon
|
|
690
|
+
? {
|
|
691
|
+
showHeader: false,
|
|
692
|
+
showPoweredBy: false,
|
|
693
|
+
promptVariant: 'lexical',
|
|
694
|
+
showModelSelector: false,
|
|
695
|
+
showToolsMenu: false,
|
|
696
|
+
showSkillsMenu: false,
|
|
697
|
+
showTokenUsage: false,
|
|
698
|
+
showContextRing: false,
|
|
699
|
+
showInformation: false,
|
|
700
|
+
showToolApprovalBanner: false,
|
|
701
|
+
showAgentsMenu: false,
|
|
702
|
+
showTurnFooter: false,
|
|
703
|
+
compact: true,
|
|
704
|
+
welcome: description,
|
|
705
|
+
backgroundColor: 'canvas.default',
|
|
706
|
+
borderRadius: 'var(--theme-radius-bubble, 16px)',
|
|
707
|
+
trailingContent: balloonApproval ? (_jsx(BalloonApprovalMessage, { approval: balloonApproval })) : undefined,
|
|
708
|
+
footerContent: assistantDecide ? (_jsx(Box, { "data-balloon-decisions": "", sx: {
|
|
709
|
+
px: 3,
|
|
710
|
+
pb: 1,
|
|
711
|
+
display: 'flex',
|
|
712
|
+
flexDirection: 'column',
|
|
713
|
+
alignItems: 'flex-end',
|
|
714
|
+
fontSize: 0,
|
|
715
|
+
'& form': { alignSelf: 'stretch' },
|
|
716
|
+
'& [data-balloon-decision="answered"]': { alignSelf: 'stretch' },
|
|
717
|
+
}, children: _jsx(DecisionAsk, { ask: assistantDecide }) })) : undefined,
|
|
718
|
+
}
|
|
719
|
+
: undefined;
|
|
624
720
|
/* The window itself; where it goes is decided below. */
|
|
625
721
|
const chatWindow = (_jsxs(Box, { ref: popupRef, className: className, sx: {
|
|
626
722
|
position: viewMode === 'panel' && !isMobile && (panelInFlow || dockedInMount)
|
|
@@ -651,8 +747,8 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
651
747
|
}
|
|
652
748
|
: getPositionStyles()
|
|
653
749
|
: {}),
|
|
654
|
-
...(
|
|
655
|
-
?
|
|
750
|
+
...(conversationBalloon
|
|
751
|
+
? conversationBalloon.place
|
|
656
752
|
: assistantMode && !isMobile
|
|
657
753
|
? getPositionStyles()
|
|
658
754
|
: {}),
|
|
@@ -746,7 +842,16 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
746
842
|
visibility: isOpen || isAnimating ? 'visible' : 'hidden',
|
|
747
843
|
pointerEvents: isOpen ? 'auto' : 'none',
|
|
748
844
|
...mobileStyles,
|
|
749
|
-
|
|
845
|
+
// The assistant's balloon: its shape and its tail toward the
|
|
846
|
+
// character, which the window's own clipping would cut off.
|
|
847
|
+
...(conversationBalloon
|
|
848
|
+
? conversationBalloonSx(conversationBalloon.tail,
|
|
849
|
+
// Out of the composer's band, or of the history's top.
|
|
850
|
+
conversationBalloon.tail.edge === 'bottom'
|
|
851
|
+
? 'canvas.subtle'
|
|
852
|
+
: 'canvas.default')
|
|
853
|
+
: {}),
|
|
854
|
+
}, "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: {
|
|
750
855
|
display: 'flex',
|
|
751
856
|
alignItems: 'center',
|
|
752
857
|
justifyContent: 'center',
|
|
@@ -759,7 +864,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
759
864
|
borderColor: 'border.muted',
|
|
760
865
|
touchAction: 'none',
|
|
761
866
|
'&:active': { cursor: 'grabbing' },
|
|
762
|
-
}, children: _jsx(GrabberIcon, { size: 16 }) })), conversation ? (_jsxs(_Fragment, { children: [showHeader ? (_jsxs(Box, { sx: {
|
|
867
|
+
}, children: _jsx(GrabberIcon, { size: 16 }) })), conversation ? (_jsxs(_Fragment, { children: [showHeader && !conversationBalloon ? (_jsxs(Box, { sx: {
|
|
763
868
|
display: 'flex',
|
|
764
869
|
alignItems: 'center',
|
|
765
870
|
gap: 2,
|
|
@@ -775,7 +880,17 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
775
880
|
overflow: 'hidden',
|
|
776
881
|
textOverflow: 'ellipsis',
|
|
777
882
|
whiteSpace: 'nowrap',
|
|
778
|
-
}, children: title }), closeButton] })) : null, _jsx(Box, { "data-floating-conversation": true, sx: {
|
|
883
|
+
}, children: title }), closeButton] })) : null, _jsx(Box, { "data-floating-conversation": true, sx: {
|
|
884
|
+
flex: '1 1 auto',
|
|
885
|
+
minHeight: 0,
|
|
886
|
+
display: 'flex',
|
|
887
|
+
...(conversationBalloon
|
|
888
|
+
? {
|
|
889
|
+
borderRadius: 'var(--theme-radius-bubble, 16px)',
|
|
890
|
+
overflow: 'hidden',
|
|
891
|
+
}
|
|
892
|
+
: {}),
|
|
893
|
+
}, children: conversation.body })] })) : (_jsx(ChatBase, { title: title, showHeader: showHeader, useStore: useStoreMode, protocol: protocol, themeVariant: themeVariant, themeOverrides: themeOverrides, colorMode: colorMode, autoFocus: isOpen, focusTrigger: focusTrigger, launching: launching, launchingMessage: launchingMessage, brandIcon: brandIcon || _jsx(AiAgentIcon, { colored: true, size: 20 }), headerButtons: {
|
|
779
894
|
showNewChat: showNewChatButton,
|
|
780
895
|
showClear: showClearButton && messages.length > 0,
|
|
781
896
|
showSettings: showSettingsButton && !!onSettingsClick,
|
|
@@ -791,7 +906,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
791
906
|
...poweredByProps,
|
|
792
907
|
}, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
|
|
793
908
|
showAvatars: true,
|
|
794
|
-
}, 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, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, children: children }))] }));
|
|
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 }))] }));
|
|
795
910
|
/*
|
|
796
911
|
* The floating popup's button. Words not yet heard make it blink for the
|
|
797
912
|
* person's attention, until the chat is opened; hovered, they show in a
|
|
@@ -839,7 +954,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
839
954
|
left: px(stageDrag.position.left),
|
|
840
955
|
top: px(stageDrag.position.top),
|
|
841
956
|
}
|
|
842
|
-
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef
|
|
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: {
|
|
843
958
|
...getPositionStyles(),
|
|
844
959
|
}, children: _jsxs(Box, { sx: {
|
|
845
960
|
position: 'relative',
|
|
@@ -68,15 +68,17 @@ export interface AssistantStageProps {
|
|
|
68
68
|
/** Open or close the conversation. */
|
|
69
69
|
onToggle: () => void;
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
* newest words, as the Office Assistant said them (T-23), or a welcome
|
|
73
|
-
*
|
|
74
|
-
* approval to answer there, with *Approve* and *Deny*.
|
|
71
|
+
* The peek while the conversation is closed: one short line — the agent's
|
|
72
|
+
* newest words, as the Office Assistant said them (T-23), or a welcome —
|
|
73
|
+
* that opens the conversation when clicked; `more` says it was cut;
|
|
74
|
+
* `approval`, an approval to answer there, with *Approve* and *Deny*.
|
|
75
75
|
*/
|
|
76
76
|
balloon?: {
|
|
77
77
|
text: string;
|
|
78
78
|
more?: boolean;
|
|
79
79
|
approval?: BalloonApproval;
|
|
80
|
+
/** Puts the peek away (a × beside its line); never an approval's. */
|
|
81
|
+
onDismiss?: () => void;
|
|
80
82
|
};
|
|
81
83
|
/** Show the balloon without being hovered: something new to say. */
|
|
82
84
|
insist?: boolean;
|
|
@@ -279,7 +279,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
279
279
|
'@media (prefers-reduced-motion: reduce)': {
|
|
280
280
|
'& *': { animation: 'none !important' },
|
|
281
281
|
},
|
|
282
|
-
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, 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) => {
|
|
282
|
+
}, 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) => {
|
|
283
283
|
pressedAt.current = { x: event.clientX, y: event.clientY };
|
|
284
284
|
onDragStart(event);
|
|
285
285
|
}, onClick: (event) => {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating assistant's conversation, open: a balloon over the character
|
|
3
|
+
* that holds the conversation itself — the history as the chat draws it, the
|
|
4
|
+
* welcome first, an approval as a message with *Approve* and *Deny* (T-23),
|
|
5
|
+
* the composer last — with no header and no footer, a tail toward the
|
|
6
|
+
* character, a close control, and a height that leaves the page in sight
|
|
7
|
+
* while the history scrolls inside it.
|
|
8
|
+
*
|
|
9
|
+
* Closed, the character peeks instead (`SpeechBalloon`): one short line, and
|
|
10
|
+
* what it needs to be answered.
|
|
11
|
+
*
|
|
12
|
+
* @module chat/assistant/ConversationBalloon
|
|
13
|
+
*/
|
|
14
|
+
import type { JSX } from 'react';
|
|
15
|
+
import { type BalloonApproval } from './state';
|
|
16
|
+
/** The open balloon's width, in pixels. */
|
|
17
|
+
export declare const CONVERSATION_BALLOON_WIDTH = 380;
|
|
18
|
+
/** The open balloon's tallest, in pixels, and its share of the window. */
|
|
19
|
+
export declare const CONVERSATION_BALLOON_MAX_HEIGHT = 560;
|
|
20
|
+
export declare const CONVERSATION_BALLOON_VIEWPORT_SHARE = 0.6;
|
|
21
|
+
/** The open balloon's height for a window this tall: 60% of it, 560px at most. */
|
|
22
|
+
export declare function conversationBalloonHeight(viewportHeight: number): number;
|
|
23
|
+
/**
|
|
24
|
+
* Where the open balloon's tail points: from its bottom edge (the balloon
|
|
25
|
+
* above the character) or its top edge (below it), `at` pixels from its
|
|
26
|
+
* left; none when the balloon stands beside the character.
|
|
27
|
+
*/
|
|
28
|
+
export type BalloonTail = {
|
|
29
|
+
edge: 'bottom' | 'top';
|
|
30
|
+
at: number;
|
|
31
|
+
} | {
|
|
32
|
+
edge: 'none';
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The tail toward a character whose middle is `characterMiddle` pixels from
|
|
36
|
+
* the window's left, under or over a balloon whose left is `balloonLeft`:
|
|
37
|
+
* kept off the balloon's rounded corners.
|
|
38
|
+
*/
|
|
39
|
+
export declare function balloonTailAt(characterMiddle: number, balloonLeft: number, width?: number): number;
|
|
40
|
+
/**
|
|
41
|
+
* The open balloon's shape: the bubble's radius, and its tail — drawn in the
|
|
42
|
+
* colour of what it leaves (`tailBg`): the composer's band under the
|
|
43
|
+
* history, the history over it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function conversationBalloonSx(tail: BalloonTail, tailBg?: string): Record<string, unknown>;
|
|
46
|
+
/** The open balloon's close control, over its top right corner. */
|
|
47
|
+
export declare function ConversationBalloonClose({ name, onClose, }: {
|
|
48
|
+
/** Who is talking: "Close the conversation with Paper clip". */
|
|
49
|
+
name?: string;
|
|
50
|
+
onClose: () => void;
|
|
51
|
+
}): JSX.Element;
|
|
52
|
+
/**
|
|
53
|
+
* An approval the agent waits on, as a message of the history: what it asks
|
|
54
|
+
* to do, the rule it was asked under, *Approve* and *Deny*, and how many more
|
|
55
|
+
* wait (T-23). Brought into view when it arrives.
|
|
56
|
+
*/
|
|
57
|
+
export declare function BalloonApprovalMessage({ approval, }: {
|
|
58
|
+
approval: BalloonApproval;
|
|
59
|
+
}): JSX.Element;
|
|
60
|
+
/** How many more wait, in two words: "2 more". */
|
|
61
|
+
export declare function moreWaiting(others: number): string;
|
|
62
|
+
/** How much of a message a peek says: its first words. */
|
|
63
|
+
export declare const PEEK_LIMIT = 60;
|
|
64
|
+
/**
|
|
65
|
+
* A message's first words, for a peek: cut at a word, with an ellipsis, and
|
|
66
|
+
* whether there was more.
|
|
67
|
+
*/
|
|
68
|
+
export declare function peekLine(text: string, limit?: number): {
|
|
69
|
+
text: string;
|
|
70
|
+
more: boolean;
|
|
71
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { Button, IconButton, Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { XIcon } from '@primer/octicons-react';
|
|
6
|
+
import { ASSISTANT_WORDS } from './state';
|
|
7
|
+
/** The open balloon's width, in pixels. */
|
|
8
|
+
export const CONVERSATION_BALLOON_WIDTH = 380;
|
|
9
|
+
/** The open balloon's tallest, in pixels, and its share of the window. */
|
|
10
|
+
export const CONVERSATION_BALLOON_MAX_HEIGHT = 560;
|
|
11
|
+
export const CONVERSATION_BALLOON_VIEWPORT_SHARE = 0.6;
|
|
12
|
+
/** The open balloon's height for a window this tall: 60% of it, 560px at most. */
|
|
13
|
+
export function conversationBalloonHeight(viewportHeight) {
|
|
14
|
+
return Math.max(240, Math.min(CONVERSATION_BALLOON_MAX_HEIGHT, Math.round(viewportHeight * CONVERSATION_BALLOON_VIEWPORT_SHARE)));
|
|
15
|
+
}
|
|
16
|
+
/** The tail's half width, in pixels. */
|
|
17
|
+
const TAIL = 7;
|
|
18
|
+
/**
|
|
19
|
+
* The tail toward a character whose middle is `characterMiddle` pixels from
|
|
20
|
+
* the window's left, under or over a balloon whose left is `balloonLeft`:
|
|
21
|
+
* kept off the balloon's rounded corners.
|
|
22
|
+
*/
|
|
23
|
+
export function balloonTailAt(characterMiddle, balloonLeft, width = CONVERSATION_BALLOON_WIDTH) {
|
|
24
|
+
return Math.min(width - 24 - TAIL, Math.max(24, characterMiddle - balloonLeft - TAIL));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The open balloon's shape: the bubble's radius, and its tail — drawn in the
|
|
28
|
+
* colour of what it leaves (`tailBg`): the composer's band under the
|
|
29
|
+
* history, the history over it.
|
|
30
|
+
*/
|
|
31
|
+
export function conversationBalloonSx(tail, tailBg = 'canvas.default') {
|
|
32
|
+
return {
|
|
33
|
+
borderRadius: 'var(--theme-radius-bubble, 16px)',
|
|
34
|
+
overflow: 'visible',
|
|
35
|
+
...(tail.edge === 'none'
|
|
36
|
+
? {}
|
|
37
|
+
: {
|
|
38
|
+
'&::after': {
|
|
39
|
+
content: '""',
|
|
40
|
+
position: 'absolute',
|
|
41
|
+
...(tail.edge === 'bottom'
|
|
42
|
+
? { bottom: `-${TAIL}px` }
|
|
43
|
+
: { top: `-${TAIL}px` }),
|
|
44
|
+
left: `${tail.at}px`,
|
|
45
|
+
width: `${TAIL * 2}px`,
|
|
46
|
+
height: `${TAIL * 2}px`,
|
|
47
|
+
bg: tailBg,
|
|
48
|
+
borderRight: '1px solid',
|
|
49
|
+
borderBottom: '1px solid',
|
|
50
|
+
borderColor: 'border.default',
|
|
51
|
+
transform: tail.edge === 'bottom' ? 'rotate(45deg)' : 'rotate(225deg)',
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The open balloon's close control, over its top right corner. */
|
|
58
|
+
export function ConversationBalloonClose({ name, onClose, }) {
|
|
59
|
+
return (_jsx(Box, { "data-conversation-balloon-close": "", sx: { position: 'absolute', top: '6px', right: '6px', zIndex: 30 }, children: _jsx(IconButton, { icon: XIcon, size: "small", variant: "invisible", "aria-label": name
|
|
60
|
+
? `Close the conversation with ${name}`
|
|
61
|
+
: 'Close the conversation', onClick: onClose, sx: { bg: 'canvas.default', borderRadius: '50%' } }) }));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* An approval the agent waits on, as a message of the history: what it asks
|
|
65
|
+
* to do, the rule it was asked under, *Approve* and *Deny*, and how many more
|
|
66
|
+
* wait (T-23). Brought into view when it arrives.
|
|
67
|
+
*/
|
|
68
|
+
export function BalloonApprovalMessage({ approval, }) {
|
|
69
|
+
const ref = useRef(null);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
ref.current?.scrollIntoView?.({ block: 'nearest' });
|
|
72
|
+
}, [approval.id]);
|
|
73
|
+
return (_jsx(Box, { ref: ref, "data-balloon-approval": approval.id, sx: { px: 3, py: 2, display: 'flex' }, children: _jsxs(Box, { sx: {
|
|
74
|
+
maxWidth: '90%',
|
|
75
|
+
px: 3,
|
|
76
|
+
py: 2,
|
|
77
|
+
bg: 'attention.subtle',
|
|
78
|
+
border: '1px solid',
|
|
79
|
+
borderColor: 'attention.muted',
|
|
80
|
+
borderRadius: 'var(--theme-radius-bubble, 16px)',
|
|
81
|
+
fontSize: 1,
|
|
82
|
+
}, children: [_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: ASSISTANT_WORDS.approval }), _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, alignItems: 'center' }, 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, { sx: { color: 'fg.muted', fontSize: 0 }, children: moreWaiting(approval.others) })) : null] })] }) }));
|
|
83
|
+
}
|
|
84
|
+
/** How many more wait, in two words: "2 more". */
|
|
85
|
+
export function moreWaiting(others) {
|
|
86
|
+
return `${others} more`;
|
|
87
|
+
}
|
|
88
|
+
/** How much of a message a peek says: its first words. */
|
|
89
|
+
export const PEEK_LIMIT = 60;
|
|
90
|
+
/**
|
|
91
|
+
* A message's first words, for a peek: cut at a word, with an ellipsis, and
|
|
92
|
+
* whether there was more.
|
|
93
|
+
*/
|
|
94
|
+
export function peekLine(text, limit = PEEK_LIMIT) {
|
|
95
|
+
const plain = text.replace(/\s+/g, ' ').trim();
|
|
96
|
+
if (plain.length <= limit) {
|
|
97
|
+
return { text: plain, more: false };
|
|
98
|
+
}
|
|
99
|
+
const cut = plain.slice(0, limit);
|
|
100
|
+
const atWord = cut.slice(0, Math.max(cut.lastIndexOf(' '), limit * 0.6));
|
|
101
|
+
return { text: `${atWord.trimEnd()}…`, more: true };
|
|
102
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A speech balloon over something that stands for the agent — the floating
|
|
3
3
|
* assistant's character, or the floating popup's button while the chat is
|
|
4
|
-
* closed: the agent's newest words, as the Office
|
|
5
|
-
* (LOOP T-23),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* closed: a peek, one short line — the agent's newest words, as the Office
|
|
5
|
+
* Assistant said them (LOOP T-23), cut to their first words, or a
|
|
6
|
+
* notification — that opens the conversation when clicked, and can be
|
|
7
|
+
* dismissed; an approval it waits on, answered there with *Approve* or
|
|
8
|
+
* *Deny*, and how many more wait; and, where the runtime can be asked and
|
|
9
|
+
* the conversation is elsewhere (the LOOP workspace), *Ask a decision*: a
|
|
10
|
+
* typed decision asked of Jev, its answer said back in the balloon.
|
|
9
11
|
*
|
|
10
12
|
* @module chat/assistant/SpeechBalloon
|
|
11
13
|
*/
|
|
@@ -34,6 +36,8 @@ export interface SpeechBalloonProps {
|
|
|
34
36
|
onDecisionActive?: (active: boolean) => void;
|
|
35
37
|
/** Wider, for the decision's form. */
|
|
36
38
|
wide?: boolean;
|
|
39
|
+
/** Puts the peek away: a × beside its line. Never for an approval. */
|
|
40
|
+
onDismissPeek?: () => void;
|
|
37
41
|
}
|
|
38
|
-
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, }: SpeechBalloonProps): JSX.Element;
|
|
42
|
+
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, onDismissPeek, }: SpeechBalloonProps): JSX.Element;
|
|
39
43
|
export default SpeechBalloon;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Button, Text } from '@primer/react';
|
|
2
|
+
import { Button, IconButton, Text } from '@primer/react';
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
import { XIcon } from '@primer/octicons-react';
|
|
4
5
|
import { ASSISTANT_WORDS } from './state';
|
|
6
|
+
import { moreWaiting } from './ConversationBalloon';
|
|
5
7
|
import { DecisionAsk } from './DecisionAsk';
|
|
6
|
-
export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, }) {
|
|
8
|
+
export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, onDismissPeek, }) {
|
|
7
9
|
return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
|
|
8
10
|
position: 'absolute',
|
|
9
11
|
// Pixels, as strings: a number here is read as the theme's space
|
|
@@ -38,15 +40,18 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
|
|
|
38
40
|
borderColor: 'border.default',
|
|
39
41
|
transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
|
|
40
42
|
},
|
|
41
|
-
}, children: [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', gap: 1 }, children: [_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", "data-balloon-more": more ? '' : undefined, onClick: onOpen, sx: {
|
|
44
|
+
flex: 1,
|
|
45
|
+
minWidth: 0,
|
|
46
|
+
m: 0,
|
|
47
|
+
p: 0,
|
|
48
|
+
border: 0,
|
|
49
|
+
bg: 'transparent',
|
|
50
|
+
color: 'inherit',
|
|
51
|
+
font: 'inherit',
|
|
52
|
+
textAlign: 'left',
|
|
53
|
+
cursor: 'pointer',
|
|
54
|
+
'&: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 })] }));
|
|
51
56
|
}
|
|
52
57
|
export default SpeechBalloon;
|
|
@@ -71,7 +71,7 @@ export interface AssistantSaying {
|
|
|
71
71
|
/** Whether there was more than the balloon holds. */
|
|
72
72
|
more: boolean;
|
|
73
73
|
}
|
|
74
|
-
/** How much of a message the
|
|
74
|
+
/** How much of a message is kept as the agent's saying before it is cut. */
|
|
75
75
|
export declare const SAYING_LIMIT = 220;
|
|
76
76
|
/**
|
|
77
77
|
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
@@ -49,7 +49,7 @@ export const ASSISTANT_WORDS = {
|
|
|
49
49
|
/** Its deployment is paused (R-17). */
|
|
50
50
|
paused: 'Paused — it answers again once it is resumed.',
|
|
51
51
|
};
|
|
52
|
-
/** How much of a message the
|
|
52
|
+
/** How much of a message is kept as the agent's saying before it is cut. */
|
|
53
53
|
export const SAYING_LIMIT = 220;
|
|
54
54
|
/**
|
|
55
55
|
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
@@ -463,6 +463,8 @@ function parseApprovedMcpToolsByServer(mcpStatusData) {
|
|
|
463
463
|
// ---------------------------------------------------------------------------
|
|
464
464
|
// ChatBase (outer wrapper — ensures QueryClient is available)
|
|
465
465
|
// ---------------------------------------------------------------------------
|
|
466
|
+
/** When the welcome is said: it has no time of its own. */
|
|
467
|
+
const WELCOME_AT = new Date(0);
|
|
466
468
|
/**
|
|
467
469
|
* ChatBase component — Universal chat panel supporting store, protocol, and custom modes.
|
|
468
470
|
*/
|
|
@@ -507,7 +509,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
|
|
|
507
509
|
// Mode selection
|
|
508
510
|
useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady, onLoadingChange, onContextSnapshot, enableStreaming = false,
|
|
509
511
|
// Extended props
|
|
510
|
-
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, 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,
|
|
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,
|
|
511
513
|
// Tool invocation hooks
|
|
512
514
|
onToolCallStart, onToolCallComplete,
|
|
513
515
|
// Identity/Authorization props
|
|
@@ -3478,7 +3480,23 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3478
3480
|
})
|
|
3479
3481
|
.filter(item => item.message)
|
|
3480
3482
|
: undefined;
|
|
3481
|
-
|
|
3483
|
+
/*
|
|
3484
|
+
* The welcome, as the first message: drawn as the agent's own words are,
|
|
3485
|
+
* never sent nor kept. With it, the history is never empty, so the empty
|
|
3486
|
+
* state gives way to it.
|
|
3487
|
+
*/
|
|
3488
|
+
const welcomedItems = welcome
|
|
3489
|
+
? [
|
|
3490
|
+
{
|
|
3491
|
+
id: '__welcome__',
|
|
3492
|
+
role: 'assistant',
|
|
3493
|
+
content: welcome,
|
|
3494
|
+
createdAt: WELCOME_AT,
|
|
3495
|
+
},
|
|
3496
|
+
...displayItems,
|
|
3497
|
+
]
|
|
3498
|
+
: displayItems;
|
|
3499
|
+
const messagesContent = children ? (children) : (_jsxs(Box, { sx: {
|
|
3482
3500
|
display: 'flex',
|
|
3483
3501
|
flexDirection: 'column',
|
|
3484
3502
|
minHeight: 0,
|
|
@@ -3494,7 +3512,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3494
3512
|
maxWidth: 920,
|
|
3495
3513
|
mx: 'auto',
|
|
3496
3514
|
bg: 'canvas.default',
|
|
3497
|
-
}, children: _jsx(ChatMessageList, { displayItems:
|
|
3515
|
+
}, 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] }));
|
|
3498
3516
|
const inputToolbar = showInput ? (_jsx(InputPrompt, { input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
|
|
3499
3517
|
/*
|
|
3500
3518
|
The same openers the empty state offers, typed into the box they would
|
|
@@ -3695,7 +3713,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3695
3713
|
top: 8,
|
|
3696
3714
|
right: 8,
|
|
3697
3715
|
zIndex: 6,
|
|
3698
|
-
}, children: _jsx(IconButton, { icon: SidebarExpandIcon, "aria-label": "Open chat", onClick: onExpandFromCollapsed, variant: "default", size: "small" }) })) : (_jsx(FloatingBrandButton, { isOpen: false, onToggle: onExpandFromCollapsed, position: "bottom-right", tooltip: "Open chat" })))] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { ref: messagesContainerRef, sx: {
|
|
3716
|
+
}, children: _jsx(IconButton, { icon: SidebarExpandIcon, "aria-label": "Open chat", onClick: onExpandFromCollapsed, variant: "default", size: "small" }) })) : (_jsx(FloatingBrandButton, { isOpen: false, onToggle: onExpandFromCollapsed, position: "bottom-right", tooltip: "Open chat" })))] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { ref: messagesContainerRef, "data-chat-history": "", sx: {
|
|
3699
3717
|
flex: 1,
|
|
3700
3718
|
flexGrow: 1,
|
|
3701
3719
|
minHeight: 0,
|
|
@@ -21,6 +21,15 @@ import { ChatFloating } from '../chat';
|
|
|
21
21
|
import { AssistantCharactersPlugin, assistantCharacterNamed, assistantCharactersOf, } from '../loop/plugins/assistant-characters';
|
|
22
22
|
import { OwlCharacterPlugin } from './utils/owlCharacterPlugin';
|
|
23
23
|
import { readAcsCharacter, readClippyCharacter, } from '../chat/assistant/formats';
|
|
24
|
+
/**
|
|
25
|
+
* The agent-runtimes server the assistant talks to: `?agentRuntimesUrl=`
|
|
26
|
+
* when given, else this machine's on port 8765. It serves the `assistant`
|
|
27
|
+
* agent, and asks Jev the decisions asked from the balloon.
|
|
28
|
+
*/
|
|
29
|
+
const SERVER = ((typeof window === 'undefined'
|
|
30
|
+
? null
|
|
31
|
+
: new URLSearchParams(window.location.search).get('agentRuntimesUrl')) ||
|
|
32
|
+
'http://127.0.0.1:8765').replace(/\/+$/, '');
|
|
24
33
|
/** The plugins this page enables: Datalayer's characters and the owl's. */
|
|
25
34
|
const reactor = buildReactorFromPlugins([
|
|
26
35
|
AssistantCharactersPlugin,
|
|
@@ -75,6 +84,8 @@ const ChatAssistantExample = () => {
|
|
|
75
84
|
catch (error) {
|
|
76
85
|
setLoadError(error instanceof Error ? error.message : String(error));
|
|
77
86
|
}
|
|
78
|
-
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, protocol: "vercel-ai", endpoint:
|
|
87
|
+
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
|
|
88
|
+
// Ask a decision beside the composer: Jev, through this server.
|
|
89
|
+
decisions: { serverUrl: SERVER } }, typeof character === 'string' ? character : character.sprite)] }) }));
|
|
79
90
|
};
|
|
80
91
|
export default ChatAssistantExample;
|
|
@@ -8,6 +8,7 @@ import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
|
|
|
8
8
|
import { pageLayoutPanelOpen, togglePagePanel, } from '@datalayer/primer-addons/lib/reactor';
|
|
9
9
|
import { AssistantStage } from '../../../chat/assistant/AssistantStage';
|
|
10
10
|
import { decisionsAskerAt } from '../../../chat/assistant/decisions';
|
|
11
|
+
import { peekLine } from '../../../chat/assistant/ConversationBalloon';
|
|
11
12
|
import { ASSISTANT_WORDS, assistantStateOf, keepAway, keptAway, latestSaying, } from '../../../chat/assistant/state';
|
|
12
13
|
import { useApproveToolRequest, useRejectToolRequest, useToolApprovalsQuery, } from '../../../hooks/useToolApprovals';
|
|
13
14
|
import { approvalsOfApp, ruleOfApproval } from '../app-rules/AppRulesCard';
|
|
@@ -142,7 +143,14 @@ export function LoopAssistant({ app, person, appId, decisions, }) {
|
|
|
142
143
|
: presence === 'waiting'
|
|
143
144
|
? { text: ASSISTANT_WORDS.waiting }
|
|
144
145
|
: unheard && saying
|
|
145
|
-
? {
|
|
146
|
+
? {
|
|
147
|
+
// A peek: the first words; the rest is in the conversation.
|
|
148
|
+
...peekLine(saying.text),
|
|
149
|
+
onDismiss: () => {
|
|
150
|
+
setHeardId(saying.id);
|
|
151
|
+
setFresh(false);
|
|
152
|
+
},
|
|
153
|
+
}
|
|
146
154
|
: { text: 'Click me to open the conversation.' };
|
|
147
155
|
const insist = state === 'greeting' ||
|
|
148
156
|
presence === 'waiting' ||
|
|
@@ -3,8 +3,12 @@ import { type ThemeVariant } from '@datalayer/primer-addons';
|
|
|
3
3
|
/** The screens, by the name a picture and a story take. */
|
|
4
4
|
export declare const REFERENCE_SCREENS: readonly ["conversation", "beside-work", "worker-activity", "approval", "tool-marks"];
|
|
5
5
|
export type ReferenceScreen = (typeof REFERENCE_SCREENS)[number];
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The floating assistant's pictures (T-27): its states — closed, a peek
|
|
8
|
+
* where it has something to say — stepped aside, and open, the conversation
|
|
9
|
+
* as its balloon (T-23).
|
|
10
|
+
*/
|
|
11
|
+
export declare const ASSISTANT_PICTURES: readonly ["idle", "thinking", "working", "waiting", "paused", "speaking", "aside", "open"];
|
|
8
12
|
export type AssistantPicture = (typeof ASSISTANT_PICTURES)[number];
|
|
9
13
|
/**
|
|
10
14
|
* The characters pictured idle, each in light and dark (T-25, T-24):
|
|
@@ -28,6 +28,8 @@ import { InputPrompt } from '../../chat/prompt/InputPrompt';
|
|
|
28
28
|
import { PresenceFace, PresenceLine } from '../../chat/presence/Presence';
|
|
29
29
|
import { AssistantStage, } from '../../chat/assistant/AssistantStage';
|
|
30
30
|
import { ASSISTANT_WORDS, } from '../../chat/assistant/state';
|
|
31
|
+
import { CONVERSATION_BALLOON_WIDTH, ConversationBalloonClose, balloonTailAt, conversationBalloonHeight, conversationBalloonSx, peekLine, } from '../../chat/assistant/ConversationBalloon';
|
|
32
|
+
import { DecisionAsk } from '../../chat/assistant/DecisionAsk';
|
|
31
33
|
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
32
34
|
import { AssistantCharactersPlugin, assistantCharacterNamed, } from '../../loop/plugins/assistant-characters';
|
|
33
35
|
import { OwlCharacterPlugin } from '../../examples/utils/owlCharacterPlugin';
|
|
@@ -39,7 +41,11 @@ export const REFERENCE_SCREENS = [
|
|
|
39
41
|
'approval',
|
|
40
42
|
'tool-marks',
|
|
41
43
|
];
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* The floating assistant's pictures (T-27): its states — closed, a peek
|
|
46
|
+
* where it has something to say — stepped aside, and open, the conversation
|
|
47
|
+
* as its balloon (T-23).
|
|
48
|
+
*/
|
|
43
49
|
export const ASSISTANT_PICTURES = [
|
|
44
50
|
'idle',
|
|
45
51
|
'thinking',
|
|
@@ -48,6 +54,7 @@ export const ASSISTANT_PICTURES = [
|
|
|
48
54
|
'paused',
|
|
49
55
|
'speaking',
|
|
50
56
|
'aside',
|
|
57
|
+
'open',
|
|
51
58
|
];
|
|
52
59
|
/**
|
|
53
60
|
* The characters pictured idle, each in light and dark (T-25, T-24):
|
|
@@ -232,9 +239,10 @@ export const REFERENCE_SCREEN_COMPONENTS = {
|
|
|
232
239
|
// The floating assistant (T-27)
|
|
233
240
|
// ---------------------------------------------------------------------------
|
|
234
241
|
const BALLOONS = {
|
|
242
|
+
// A new message, peeked by its first words (T-23).
|
|
235
243
|
speaking: {
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
...peekLine('Three customers wrote about late deliveries this week: Ada, Grace and Alan.'),
|
|
245
|
+
onDismiss: () => undefined,
|
|
238
246
|
},
|
|
239
247
|
// An approval it waits on, answered in the balloon (T-23).
|
|
240
248
|
waiting: {
|
|
@@ -257,9 +265,9 @@ const BALLOONS = {
|
|
|
257
265
|
*/
|
|
258
266
|
export function AssistantScreen({ picture, }) {
|
|
259
267
|
const stageRef = useRef(null);
|
|
260
|
-
const state = picture === 'aside' ? 'idle' : picture;
|
|
268
|
+
const state = picture === 'aside' || picture === 'open' ? 'idle' : picture;
|
|
261
269
|
const balloon = BALLOONS[picture];
|
|
262
|
-
return (_jsxs(Box, { sx: { flex: 1, position: 'relative' }, children: [picture === 'aside' && (_jsxs(Box, { role: "dialog", "aria-label": "A dialog over the assistant", sx: {
|
|
270
|
+
return (_jsxs(Box, { sx: { flex: 1, position: 'relative' }, children: [picture === 'open' && _jsx(OpenConversation, {}), picture === 'aside' && (_jsxs(Box, { role: "dialog", "aria-label": "A dialog over the assistant", sx: {
|
|
263
271
|
position: 'fixed',
|
|
264
272
|
right: 24,
|
|
265
273
|
bottom: 24,
|
|
@@ -272,7 +280,56 @@ export function AssistantScreen({ picture, }) {
|
|
|
272
280
|
borderColor: 'border.default',
|
|
273
281
|
borderRadius: 'var(--theme-radius-card, 6px)',
|
|
274
282
|
boxShadow: 'var(--theme-shadow, none)',
|
|
275
|
-
}, children: [_jsx(Text, { sx: { fontWeight: 600 }, children: "A dialog" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1 }, children: "The assistant steps aside while this is open." })] })), _jsx(AssistantStage, { character: "paperclip", state: state, place: { right: 48, bottom: 48 }, stageRef: stageRef, onDragStart: () => undefined, open:
|
|
283
|
+
}, children: [_jsx(Text, { sx: { fontWeight: 600 }, children: "A dialog" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1 }, children: "The assistant steps aside while this is open." })] })), _jsx(AssistantStage, { character: "paperclip", state: state, place: { right: 48, bottom: 48 }, stageRef: stageRef, onDragStart: () => undefined, open: picture === 'open', onToggle: () => undefined, balloon: balloon, insist: !!balloon, onDismiss: () => undefined })] }));
|
|
284
|
+
}
|
|
285
|
+
/** What the open picture's conversation holds. */
|
|
286
|
+
const OPEN_CONVERSATION = [
|
|
287
|
+
said('w', 'assistant', 'Hello! Ask me anything about this page.'),
|
|
288
|
+
said('o1', 'user', 'Which customers wrote about late deliveries this week?'),
|
|
289
|
+
said('o2', 'assistant', 'Three of them: **Ada**, **Grace** and **Alan**. Ada’s parcel is two days late; the other two are waiting for a reply.'),
|
|
290
|
+
];
|
|
291
|
+
/**
|
|
292
|
+
* The conversation open, as the assistant's balloon (T-23): over the
|
|
293
|
+
* character in the page's corner, its tail toward it, the history with the
|
|
294
|
+
* welcome first, *Ask a decision* beside the composer, the Lexical composer
|
|
295
|
+
* last, and its close in the corner — no header, no footer.
|
|
296
|
+
*/
|
|
297
|
+
function OpenConversation() {
|
|
298
|
+
const endRef = useRef(null);
|
|
299
|
+
const [input, setInput] = useState('');
|
|
300
|
+
const height = conversationBalloonHeight(window.innerHeight);
|
|
301
|
+
const left = window.innerWidth - 48 - CONVERSATION_BALLOON_WIDTH;
|
|
302
|
+
return (_jsxs(Box, { "data-conversation-balloon": "", sx: {
|
|
303
|
+
position: 'fixed',
|
|
304
|
+
right: '48px',
|
|
305
|
+
bottom: `${48 + 88 + 16}px`,
|
|
306
|
+
width: `${CONVERSATION_BALLOON_WIDTH}px`,
|
|
307
|
+
height: `${height}px`,
|
|
308
|
+
zIndex: 1001,
|
|
309
|
+
display: 'flex',
|
|
310
|
+
flexDirection: 'column',
|
|
311
|
+
bg: 'canvas.default',
|
|
312
|
+
border: '1px solid',
|
|
313
|
+
borderColor: 'border.default',
|
|
314
|
+
boxShadow: 'shadow.extra-large',
|
|
315
|
+
...conversationBalloonSx({
|
|
316
|
+
edge: 'bottom',
|
|
317
|
+
at: balloonTailAt(window.innerWidth - 48 - 44, left),
|
|
318
|
+
}, 'canvas.subtle'),
|
|
319
|
+
}, children: [_jsx(ConversationBalloonClose, { onClose: () => undefined }), _jsxs(Box, { sx: {
|
|
320
|
+
flex: 1,
|
|
321
|
+
minHeight: 0,
|
|
322
|
+
display: 'flex',
|
|
323
|
+
flexDirection: 'column',
|
|
324
|
+
borderRadius: 'var(--theme-radius-bubble, 16px)',
|
|
325
|
+
overflow: 'hidden',
|
|
326
|
+
}, children: [_jsx(Box, { sx: { flex: 1, minHeight: 0, overflow: 'auto' }, children: _jsx(ChatMessageList, { displayItems: OPEN_CONVERSATION, isLoading: false, isStreaming: false, showLoadingIndicator: false, hideMessagesAfterToolUI: false, avatarConfig: { ...AVATARS, showAvatars: true }, padding: 2, emptyContent: null, messagesEndRef: endRef, onRespond: async () => undefined }) }), _jsx(Box, { sx: {
|
|
327
|
+
px: 3,
|
|
328
|
+
pb: 1,
|
|
329
|
+
display: 'flex',
|
|
330
|
+
justifyContent: 'flex-end',
|
|
331
|
+
fontSize: 0,
|
|
332
|
+
}, children: _jsx(DecisionAsk, { ask: async () => ({}) }) }), _jsx(InputPrompt, { input: input, setInput: setInput, isLoading: false, connectionConfirmed: true, placeholder: "Type a message...", autoFocus: false, padding: 2, promptVariant: "lexical", onSend: () => undefined, onStop: () => undefined, showTokenUsage: false, showModelSelector: false, showToolsMenu: false, showSkillsMenu: false, showAgentsMenu: false, codemodeEnabled: false, hasConfigData: false, hasSkillsData: false, isA2AProtocol: false })] })] }));
|
|
276
333
|
}
|
|
277
334
|
// ---------------------------------------------------------------------------
|
|
278
335
|
// The characters (T-25, T-24)
|
package/lib/types/chat.d.ts
CHANGED
|
@@ -964,6 +964,18 @@ export interface ChatBaseProps {
|
|
|
964
964
|
notebookToolSurfacesId?: string;
|
|
965
965
|
/** Custom footer content (rendered above input) */
|
|
966
966
|
footerContent?: ReactNode;
|
|
967
|
+
/**
|
|
968
|
+
* The agent's welcome, said as the first message of the history: drawn as
|
|
969
|
+
* the chat draws the agent's messages, and never sent nor kept. The floating
|
|
970
|
+
* assistant's balloon opens on it (LOOP T-23).
|
|
971
|
+
*/
|
|
972
|
+
welcome?: string;
|
|
973
|
+
/**
|
|
974
|
+
* What follows the last message, inside the history and scrolled with it:
|
|
975
|
+
* the floating assistant's balloon holds an approval there, as a message
|
|
976
|
+
* with *Approve* and *Deny* (T-23).
|
|
977
|
+
*/
|
|
978
|
+
trailingContent?: ReactNode;
|
|
967
979
|
/**
|
|
968
980
|
* Show the information icon in the header.
|
|
969
981
|
* When clicked, fires onInformationClick.
|