@datalayer/agent-runtimes 1.3.28 → 1.3.30
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/AgentNode.js +14 -33
- package/lib/chat/ChatFloating.d.ts +12 -2
- package/lib/chat/ChatFloating.js +196 -25
- package/lib/chat/assistant/AssistantStage.d.ts +70 -0
- package/lib/chat/assistant/AssistantStage.js +192 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +26 -0
- package/lib/chat/assistant/SpeechBalloon.js +46 -0
- package/lib/chat/assistant/SpriteCharacter.d.ts +26 -0
- package/lib/chat/assistant/SpriteCharacter.js +73 -0
- package/lib/chat/assistant/characters.d.ts +30 -0
- package/lib/chat/assistant/characters.js +41 -0
- package/lib/chat/assistant/formats/acs.d.ts +88 -0
- package/lib/chat/assistant/formats/acs.js +493 -0
- package/lib/chat/assistant/formats/agentCompression.d.ts +5 -0
- package/lib/chat/assistant/formats/agentCompression.js +92 -0
- package/lib/chat/assistant/formats/blobs.d.ts +6 -0
- package/lib/chat/assistant/formats/blobs.js +52 -0
- package/lib/chat/assistant/formats/clippy.d.ts +21 -0
- package/lib/chat/assistant/formats/clippy.js +167 -0
- package/lib/chat/assistant/formats/index.d.ts +13 -0
- package/lib/chat/assistant/formats/index.js +15 -0
- package/lib/chat/assistant/formats/objectLiteral.d.ts +23 -0
- package/lib/chat/assistant/formats/objectLiteral.js +233 -0
- package/lib/chat/assistant/formats/stateAnimations.d.ts +16 -0
- package/lib/chat/assistant/formats/stateAnimations.js +115 -0
- package/lib/chat/assistant/formats/types.d.ts +56 -0
- package/lib/chat/assistant/formats/types.js +11 -0
- package/lib/chat/assistant/state.d.ts +62 -0
- package/lib/chat/assistant/state.js +116 -0
- package/lib/chat/base/ChatBase.js +41 -32
- package/lib/chat/base/modelChoice.d.ts +13 -0
- package/lib/chat/base/modelChoice.js +13 -0
- package/lib/chat/header/ChatViewModeToggle.js +6 -3
- package/lib/{loop/plugins/chat → chat/presence}/Presence.js +1 -1
- package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.d.ts +7 -1
- package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.js +18 -0
- package/lib/chat/viewModes.d.ts +6 -0
- package/lib/chat/viewModes.js +10 -1
- package/lib/examples/ChatAssistantExample.d.ts +8 -0
- package/lib/examples/ChatAssistantExample.js +59 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/main.js +3 -0
- package/lib/loop/apps/AppRenderer.d.ts +34 -2
- package/lib/loop/apps/AppRenderer.js +96 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/appspec.d.ts +5 -1
- package/lib/loop/apps/appspec.js +21 -1
- package/lib/loop/apps/checks.js +3 -1
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/index.d.ts +23 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/embed/AppEmbed.d.ts +42 -0
- package/lib/loop/embed/AppEmbed.js +205 -0
- package/lib/loop/embed/datalayer-app.d.ts +1 -0
- package/lib/loop/embed/datalayer-app.js +16 -0
- package/lib/loop/embed/element.d.ts +57 -0
- package/lib/loop/embed/element.js +405 -0
- package/lib/loop/embed/embedConfig.d.ts +120 -0
- package/lib/loop/embed/embedConfig.js +148 -0
- package/lib/loop/embed/embedTheme.d.ts +24 -0
- package/lib/loop/embed/embedTheme.js +83 -0
- package/lib/loop/index.d.ts +6 -2
- package/lib/loop/index.js +8 -2
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
- package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -1
- package/lib/loop/plugins/agents/switchable.d.ts +7 -0
- package/lib/loop/plugins/agents/switchable.js +91 -88
- package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
- package/lib/loop/plugins/app-page/AppPage.js +53 -0
- package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPage.js +443 -0
- package/lib/loop/plugins/app-page/index.d.ts +8 -0
- package/lib/loop/plugins/app-page/index.js +34 -0
- package/lib/loop/plugins/assistant-characters/index.d.ts +30 -0
- package/lib/loop/plugins/assistant-characters/index.js +41 -0
- package/lib/loop/plugins/chat/ChatView.js +50 -39
- package/lib/loop/plugins/chat/index.d.ts +1 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
- package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
- package/lib/loop/plugins/page-layout/index.d.ts +13 -0
- package/lib/loop/plugins/page-layout/index.js +15 -2
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +14 -0
- package/lib/specs/agents/agents.js +4 -0
- package/lib/specs/apps.d.ts +13 -1
- package/lib/specs/apps.js +2715 -373
- package/lib/specs/appspecSchema.js +21 -3
- package/lib/types/agentspecs.d.ts +21 -1
- package/lib/types/chat.d.ts +1 -1
- package/lib/types/config.d.ts +12 -0
- package/package.json +2 -1
- package/scripts/codegen/generate_agents.py +50 -2
- package/scripts/codegen/generate_apps.py +36 -6
- /package/lib/{loop/plugins/chat → chat/presence}/Presence.d.ts +0 -0
package/lib/AgentNode.js
CHANGED
|
@@ -345,7 +345,8 @@ export function AgentNode() {
|
|
|
345
345
|
]);
|
|
346
346
|
const [inferenceProvider, setInferenceProvider] = useState('datalayer');
|
|
347
347
|
const [inferenceModels, setInferenceModels] = useState([]);
|
|
348
|
-
|
|
348
|
+
// The runtime's sentence about its models, or why it gave none.
|
|
349
|
+
const [inferenceModelsNote, setInferenceModelsNote] = useState(null);
|
|
349
350
|
const [configurationLoaded, setConfigurationLoaded] = useState(false);
|
|
350
351
|
const [isActiveAgentRunning, setIsActiveAgentRunning] = useState(false);
|
|
351
352
|
// The protocol the active agent actually speaks, as reported by the node.
|
|
@@ -712,49 +713,26 @@ export function AgentNode() {
|
|
|
712
713
|
useEffect(() => {
|
|
713
714
|
if (inferenceProvider !== 'datalayer') {
|
|
714
715
|
setInferenceModels([]);
|
|
715
|
-
|
|
716
|
+
setInferenceModelsNote(null);
|
|
716
717
|
return;
|
|
717
718
|
}
|
|
719
|
+
// The runtime's answer, as it had it from ai-inference: no list of its
|
|
720
|
+
// own when that answer is empty or did not come — the sentence says why.
|
|
718
721
|
const loadInferenceModels = async () => {
|
|
719
722
|
try {
|
|
720
723
|
const response = await fetch(`${AGENT_RUNTIMES_BASE_URL}/api/v1/configure/inference/models`);
|
|
721
724
|
if (!response.ok) {
|
|
722
725
|
setInferenceModels([]);
|
|
723
|
-
|
|
726
|
+
setInferenceModelsNote(`The runtime did not list its models (${response.status}).`);
|
|
724
727
|
return;
|
|
725
728
|
}
|
|
726
729
|
const payload = await response.json();
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
: [];
|
|
730
|
-
const fromBedrock = Array.isArray(payload.bedrock_anthropic_models)
|
|
731
|
-
? payload.bedrock_anthropic_models.filter(Boolean)
|
|
732
|
-
: [];
|
|
733
|
-
const fallback = [
|
|
734
|
-
'bedrock/us.anthropic.claude-3-5-sonnet-20240620-v1:0',
|
|
735
|
-
'bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
|
736
|
-
'bedrock/us.anthropic.claude-sonnet-4-20250514-v1:0',
|
|
737
|
-
];
|
|
738
|
-
const models = fromModels.length > 0
|
|
739
|
-
? fromModels
|
|
740
|
-
: fromBedrock.length > 0
|
|
741
|
-
? fromBedrock
|
|
742
|
-
: fallback;
|
|
743
|
-
setInferenceModels(models);
|
|
744
|
-
const specDefault = Array.isArray(payload.bedrock_anthropic_model_specs)
|
|
745
|
-
? payload.bedrock_anthropic_model_specs.find(s => s?.default)?.id
|
|
746
|
-
: undefined;
|
|
747
|
-
const selected = (specDefault && models.includes(specDefault) ? specDefault : null) ||
|
|
748
|
-
(payload.default_model && models.includes(payload.default_model)
|
|
749
|
-
? payload.default_model
|
|
750
|
-
: null) ||
|
|
751
|
-
models[0] ||
|
|
752
|
-
null;
|
|
753
|
-
setInferenceDefaultModel(selected);
|
|
730
|
+
setInferenceModels(Array.isArray(payload.models) ? payload.models.filter(Boolean) : []);
|
|
731
|
+
setInferenceModelsNote(payload.note ?? null);
|
|
754
732
|
}
|
|
755
|
-
catch {
|
|
733
|
+
catch (error) {
|
|
756
734
|
setInferenceModels([]);
|
|
757
|
-
|
|
735
|
+
setInferenceModelsNote(`The runtime did not list its models (${String(error)}).`);
|
|
758
736
|
}
|
|
759
737
|
};
|
|
760
738
|
loadInferenceModels();
|
|
@@ -1280,7 +1258,10 @@ export function AgentNode() {
|
|
|
1280
1258
|
display: 'flex',
|
|
1281
1259
|
flexDirection: 'column',
|
|
1282
1260
|
gap: 1,
|
|
1283
|
-
}, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "
|
|
1261
|
+
}, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "Models ai-inference serves" }), inferenceModels.length === 0 ? (_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: inferenceModelsNote ??
|
|
1262
|
+
'The runtime has not said which models ai-inference serves.' })) : (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { children: inferenceModels.length === 1
|
|
1263
|
+
? inferenceModels[0]
|
|
1264
|
+
: `${inferenceModels.length} models` }), _jsx(ActionMenu.Overlay, { width: "large", children: _jsx(ActionList, { children: inferenceModels.map(model => (_jsx(ActionList.Item, { inactiveText: "Selection is locked", children: model }, model))) }) })] }))] }))] }), iamUser ? (_jsxs(Box, { sx: {
|
|
1284
1265
|
border: '1px solid',
|
|
1285
1266
|
borderColor: 'border.default',
|
|
1286
1267
|
borderRadius: 2,
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @module chat/ChatFloating
|
|
13
13
|
*/
|
|
14
14
|
import React from 'react';
|
|
15
|
+
import { type AssistantCharacter } from './assistant/characters';
|
|
16
|
+
import type { AssistantCharacterData } from './assistant/formats/types';
|
|
15
17
|
import type { ChatCommonProps, ChatViewMode } from '../types';
|
|
16
18
|
/**
|
|
17
19
|
* ChatFloating props — extends ChatCommonProps with floating/popup-specific configuration.
|
|
@@ -67,10 +69,18 @@ export interface ChatFloatingProps extends ChatCommonProps {
|
|
|
67
69
|
* - 'floating': Full-height floating panel (pinned to right edge with offset)
|
|
68
70
|
* - 'floating-small': Standard floating popup
|
|
69
71
|
* - 'floating-draggable': The popup with a handle, movable about the viewport
|
|
72
|
+
* - 'assistant': A character on the page that speaks in a balloon (LOOP T-21)
|
|
70
73
|
* - 'panel': Full-height side panel (right edge, no floating offset)
|
|
71
74
|
* @default 'floating'
|
|
72
75
|
*/
|
|
73
|
-
defaultViewMode?: 'floating' | 'floating-small' | 'floating-draggable' | 'panel';
|
|
76
|
+
defaultViewMode?: 'floating' | 'floating-small' | 'floating-draggable' | 'assistant' | 'panel';
|
|
77
|
+
/**
|
|
78
|
+
* The character of the floating assistant: one Datalayer ships, by id
|
|
79
|
+
* (`ASSISTANT_CHARACTERS`, LOOP T-25), or one a person loaded from a file
|
|
80
|
+
* they hold the rights to (`readClippyCharacter`, `readAcsCharacter`, T-26).
|
|
81
|
+
* @default 'paperclip'
|
|
82
|
+
*/
|
|
83
|
+
assistantCharacter?: string | AssistantCharacter | AssistantCharacterData;
|
|
74
84
|
/**
|
|
75
85
|
* Callback when the user switches view mode via the header toggle.
|
|
76
86
|
* The parent component receives the new ChatViewMode value.
|
|
@@ -99,5 +109,5 @@ export interface ChatFloatingProps extends ChatCommonProps {
|
|
|
99
109
|
* ChatFloating component
|
|
100
110
|
* A floating chat window built on ChatBase
|
|
101
111
|
*/
|
|
102
|
-
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, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, }: ChatFloatingProps): React.JSX.Element;
|
|
112
|
+
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, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, }: ChatFloatingProps): React.JSX.Element;
|
|
103
113
|
export default ChatFloating;
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -26,6 +26,11 @@ import { ChatBase } from './base/ChatBase';
|
|
|
26
26
|
import { ButtonGlow } from './display/ButtonGlow';
|
|
27
27
|
import { useViewportDrag } from './useViewportDrag';
|
|
28
28
|
import { disabledChatViewModes, resolveMountPoint } from './viewModes';
|
|
29
|
+
import { AssistantStage } from './assistant/AssistantStage';
|
|
30
|
+
import { SpeechBalloon } from './assistant/SpeechBalloon';
|
|
31
|
+
import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
|
|
32
|
+
import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, } from './assistant/state';
|
|
33
|
+
import { presenceState, presenceToolOf, } from './presence/presenceStatus';
|
|
29
34
|
import { useChatOpen, useChatMessages, useChatStore, } from '../stores/chatStore';
|
|
30
35
|
import { useChatKeyboardShortcuts, getShortcutDisplay, } from '@datalayer/core/lib/hooks';
|
|
31
36
|
/**
|
|
@@ -47,7 +52,7 @@ function useIsMobile(breakpoint = 640) {
|
|
|
47
52
|
* ChatFloating component
|
|
48
53
|
* A floating chat window built on ChatBase
|
|
49
54
|
*/
|
|
50
|
-
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, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, }) {
|
|
55
|
+
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, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, }) {
|
|
51
56
|
// Store-based state
|
|
52
57
|
const storeIsOpen = useChatOpen();
|
|
53
58
|
const storeMessages = useChatMessages();
|
|
@@ -96,6 +101,88 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
96
101
|
resetDrag();
|
|
97
102
|
}
|
|
98
103
|
}, [viewMode, resetDrag]);
|
|
104
|
+
/*
|
|
105
|
+
* The floating assistant (LOOP T-21, T-22, T-27): a character that acts out
|
|
106
|
+
* the presence of what answers — read here from the chat's own loading and
|
|
107
|
+
* items — greets when it arrives, says goodbye when sent away, and is
|
|
108
|
+
* dragged about by itself, the conversation following it.
|
|
109
|
+
*/
|
|
110
|
+
const ASSISTANT_SIZE = 88;
|
|
111
|
+
const assistantMode = viewMode === 'assistant' && !isMobile;
|
|
112
|
+
const [chatBusy, setChatBusy] = useState(false);
|
|
113
|
+
const [chatTool, setChatTool] = useState({
|
|
114
|
+
open: false,
|
|
115
|
+
pendingApproval: false,
|
|
116
|
+
});
|
|
117
|
+
const [answering, setAnswering] = useState(false);
|
|
118
|
+
const [arriving, setArriving] = useState(false);
|
|
119
|
+
const [leaving, setLeaving] = useState(false);
|
|
120
|
+
const [assistantAway, setAssistantAway] = useState(keptAway);
|
|
121
|
+
const assistantShown = assistantMode && assistantAway === 'none';
|
|
122
|
+
const callAssistantBack = useCallback(() => {
|
|
123
|
+
keepAway('none');
|
|
124
|
+
setAssistantAway('none');
|
|
125
|
+
}, []);
|
|
126
|
+
/* The agent's newest words, said in the balloon until heard (T-23). */
|
|
127
|
+
const [saying, setSaying] = useState();
|
|
128
|
+
const [heardId, setHeardId] = useState();
|
|
129
|
+
const [freshSaying, setFreshSaying] = useState(false);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!saying || isOpen) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setFreshSaying(true);
|
|
135
|
+
const timer = setTimeout(() => setFreshSaying(false), 12000);
|
|
136
|
+
return () => clearTimeout(timer);
|
|
137
|
+
}, [saying?.id, saying?.text, isOpen]);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (isOpen && saying) {
|
|
140
|
+
setHeardId(saying.id);
|
|
141
|
+
}
|
|
142
|
+
}, [isOpen, saying]);
|
|
143
|
+
const stageRef = useRef(null);
|
|
144
|
+
const stageDrag = useViewportDrag(stageRef);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!assistantShown) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
setArriving(true);
|
|
150
|
+
const timer = setTimeout(() => setArriving(false), 1800);
|
|
151
|
+
return () => clearTimeout(timer);
|
|
152
|
+
}, [assistantShown]);
|
|
153
|
+
const assistantState = assistantStateOf(presenceState(chatBusy, chatTool), {
|
|
154
|
+
arriving,
|
|
155
|
+
leaving,
|
|
156
|
+
speaking: answering,
|
|
157
|
+
});
|
|
158
|
+
const unheard = !!saying && saying.id !== heardId;
|
|
159
|
+
const assistantBalloon = assistantState === 'waiting'
|
|
160
|
+
? { text: 'Waiting for you — open the conversation to answer.' }
|
|
161
|
+
: unheard && saying
|
|
162
|
+
? { text: saying.text, more: saying.more }
|
|
163
|
+
: { text: description };
|
|
164
|
+
const balloonInsists = assistantState === 'greeting' ||
|
|
165
|
+
assistantState === 'waiting' ||
|
|
166
|
+
(unheard && (assistantState === 'speaking' || freshSaying));
|
|
167
|
+
const panelOnLoadingChange = panelProps?.onLoadingChange;
|
|
168
|
+
const panelOnDisplayItemsChange = panelProps?.onDisplayItemsChange;
|
|
169
|
+
const handleLoadingChange = useCallback((loading) => {
|
|
170
|
+
setChatBusy(loading);
|
|
171
|
+
panelOnLoadingChange?.(loading);
|
|
172
|
+
}, [panelOnLoadingChange]);
|
|
173
|
+
const handleDisplayItemsChange = useCallback((items) => {
|
|
174
|
+
const tool = presenceToolOf(items);
|
|
175
|
+
setChatTool(previous => previous.open === tool.open &&
|
|
176
|
+
previous.pendingApproval === tool.pendingApproval
|
|
177
|
+
? previous
|
|
178
|
+
: tool);
|
|
179
|
+
setAnswering(newestIsAnswer(items));
|
|
180
|
+
const said = latestSaying(items);
|
|
181
|
+
setSaying(previous => previous?.id === said?.id && previous?.text === said?.text
|
|
182
|
+
? previous
|
|
183
|
+
: said);
|
|
184
|
+
panelOnDisplayItemsChange?.(items);
|
|
185
|
+
}, [panelOnDisplayItemsChange]);
|
|
99
186
|
// Handle view mode changes from the header segmented toggle
|
|
100
187
|
const handleChatViewModeChange = useCallback((mode) => {
|
|
101
188
|
if (mode === 'sidebar' && sidebarDisabled) {
|
|
@@ -115,12 +202,16 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
115
202
|
setFocusTrigger(prev => prev + 1);
|
|
116
203
|
}
|
|
117
204
|
else {
|
|
118
|
-
// The floating modes stay within ChatFloating
|
|
205
|
+
// The floating modes stay within ChatFloating; picking the assistant
|
|
206
|
+
// calls it back, however long it was sent away for (T-27).
|
|
207
|
+
if (mode === 'assistant') {
|
|
208
|
+
callAssistantBack();
|
|
209
|
+
}
|
|
119
210
|
setViewMode(mode);
|
|
120
211
|
setFocusTrigger(prev => prev + 1);
|
|
121
212
|
onViewModeChange?.(mode);
|
|
122
213
|
}
|
|
123
|
-
}, [onViewModeChange, sidebarDisabled]);
|
|
214
|
+
}, [onViewModeChange, sidebarDisabled, callAssistantBack]);
|
|
124
215
|
// Detect whether the chat is mounted inside a parent that is intentionally
|
|
125
216
|
// configured for docked side panels (Notebook example lays the chat out as a
|
|
126
217
|
// flex sibling). Those hosts dock the panel in-flow. Plain content pages
|
|
@@ -272,6 +363,42 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
272
363
|
// Reset animating after duration
|
|
273
364
|
setTimeout(() => setIsAnimating(false), animationDuration);
|
|
274
365
|
}, [isOpen, setIsOpen, onOpen, onClose, animationDuration]);
|
|
366
|
+
/*
|
|
367
|
+
* The assistant sent away (T-27): goodbye first, then the popup's round
|
|
368
|
+
* button in its place — which calls it back when it went for the page, and
|
|
369
|
+
* opens the chat when it went for the session or for good.
|
|
370
|
+
*/
|
|
371
|
+
const dismissAssistant = useCallback((away) => {
|
|
372
|
+
if (isOpen) {
|
|
373
|
+
handleToggle();
|
|
374
|
+
}
|
|
375
|
+
keepAway(away);
|
|
376
|
+
setLeaving(true);
|
|
377
|
+
setTimeout(() => {
|
|
378
|
+
setLeaving(false);
|
|
379
|
+
setAssistantAway(away);
|
|
380
|
+
}, 600);
|
|
381
|
+
}, [isOpen, handleToggle]);
|
|
382
|
+
/*
|
|
383
|
+
* The conversation as the assistant's balloon: above the character, its
|
|
384
|
+
* right edge on the character's, and never off the screen.
|
|
385
|
+
*/
|
|
386
|
+
const balloonPlace = () => {
|
|
387
|
+
const widthPx = typeof width === 'number' ? width : 400;
|
|
388
|
+
const heightPx = typeof height === 'number' ? height : 550;
|
|
389
|
+
if (stageDrag.position) {
|
|
390
|
+
return {
|
|
391
|
+
left: Math.max(8, stageDrag.position.left + ASSISTANT_SIZE - widthPx),
|
|
392
|
+
top: Math.max(8, stageDrag.position.top - heightPx - 16),
|
|
393
|
+
right: 'auto',
|
|
394
|
+
bottom: 'auto',
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const corner = getPositionStyles();
|
|
398
|
+
return position.startsWith('bottom')
|
|
399
|
+
? { ...corner, bottom: offset + ASSISTANT_SIZE + 16 }
|
|
400
|
+
: { ...corner, top: offset + ASSISTANT_SIZE + 16 };
|
|
401
|
+
};
|
|
275
402
|
// Handle new chat
|
|
276
403
|
const handleNewChat = useCallback(() => {
|
|
277
404
|
if (useStoreMode) {
|
|
@@ -437,6 +564,11 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
437
564
|
}
|
|
438
565
|
: getPositionStyles()
|
|
439
566
|
: {}),
|
|
567
|
+
...(assistantShown
|
|
568
|
+
? balloonPlace()
|
|
569
|
+
: assistantMode && !isMobile
|
|
570
|
+
? getPositionStyles()
|
|
571
|
+
: {}),
|
|
440
572
|
...(viewMode === 'panel' && !isMobile
|
|
441
573
|
? {
|
|
442
574
|
...(panelInFlow || dockedInMount
|
|
@@ -468,7 +600,8 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
468
600
|
? `${popupWidth}px`
|
|
469
601
|
: popupWidth
|
|
470
602
|
: (viewMode === 'floating-small' ||
|
|
471
|
-
viewMode === 'floating-draggable'
|
|
603
|
+
viewMode === 'floating-draggable' ||
|
|
604
|
+
viewMode === 'assistant') &&
|
|
472
605
|
!isMobile
|
|
473
606
|
? typeof popupWidth === 'number'
|
|
474
607
|
? `${popupWidth}px`
|
|
@@ -487,7 +620,8 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
487
620
|
: viewMode === 'floating' && !isMobile
|
|
488
621
|
? '100%'
|
|
489
622
|
: (viewMode === 'floating-small' ||
|
|
490
|
-
viewMode === 'floating-draggable'
|
|
623
|
+
viewMode === 'floating-draggable' ||
|
|
624
|
+
viewMode === 'assistant') &&
|
|
491
625
|
!isMobile
|
|
492
626
|
? typeof popupHeight === 'number'
|
|
493
627
|
? `${popupHeight}px`
|
|
@@ -502,7 +636,11 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
502
636
|
bg: 'canvas.default',
|
|
503
637
|
border: '1px solid',
|
|
504
638
|
borderColor: 'border.default',
|
|
505
|
-
borderRadius: viewMode === 'panel' || isMobile
|
|
639
|
+
borderRadius: viewMode === 'panel' || isMobile
|
|
640
|
+
? 0
|
|
641
|
+
: assistantMode
|
|
642
|
+
? 'var(--theme-radius-bubble, 16px)'
|
|
643
|
+
: '12px',
|
|
506
644
|
boxShadow: viewMode === 'panel' ? 'shadow.none' : 'shadow.extra-large',
|
|
507
645
|
overflow: 'hidden',
|
|
508
646
|
transform: viewMode === 'panel'
|
|
@@ -550,30 +688,56 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
550
688
|
...poweredByProps,
|
|
551
689
|
}, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
|
|
552
690
|
showAvatars: true,
|
|
553
|
-
}, 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, children: children })] }));
|
|
554
|
-
|
|
691
|
+
}, 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 })] }));
|
|
692
|
+
/*
|
|
693
|
+
* The floating popup's button. Words not yet heard make it blink for the
|
|
694
|
+
* person's attention, until the chat is opened; hovered, they show in a
|
|
695
|
+
* balloon above it in place of the tooltip.
|
|
696
|
+
*/
|
|
697
|
+
const chatButton = (_jsx(IconButton, { icon: buttonIcon ? buttonIcon : CommentDiscussionIcon, "aria-label": buttonTooltip, onClick: assistantMode && assistantAway === 'page'
|
|
698
|
+
? callAssistantBack
|
|
699
|
+
: handleToggle, size: "large", sx: {
|
|
700
|
+
width: 56,
|
|
701
|
+
height: 56,
|
|
702
|
+
borderRadius: '50%',
|
|
703
|
+
bg: brandColor || 'accent.emphasis',
|
|
704
|
+
color: 'fg.onEmphasis',
|
|
705
|
+
boxShadow: 'shadow.large',
|
|
706
|
+
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
707
|
+
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
|
|
708
|
+
// Words not yet heard: the circle blinks for the person's
|
|
709
|
+
// attention, and stops once the chat is opened.
|
|
710
|
+
...(unheard
|
|
711
|
+
? {
|
|
712
|
+
animation: 'chatFloatingBlink 1.1s ease-in-out infinite',
|
|
713
|
+
'@keyframes chatFloatingBlink': {
|
|
714
|
+
'0%, 100%': {
|
|
715
|
+
filter: 'brightness(1)',
|
|
716
|
+
boxShadow: '0 0 0 0 rgba(0,0,0,0)',
|
|
717
|
+
},
|
|
718
|
+
'50%': {
|
|
719
|
+
filter: 'brightness(1.25)',
|
|
720
|
+
boxShadow: '0 0 0 6px var(--bgColor-accent-muted, rgba(84,174,255,0.4))',
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
|
724
|
+
}
|
|
725
|
+
: {}),
|
|
726
|
+
'&:hover': {
|
|
727
|
+
bg: brandColor || 'accent.emphasis',
|
|
728
|
+
boxShadow: 'shadow.extra-large',
|
|
729
|
+
},
|
|
730
|
+
} }));
|
|
731
|
+
return (_jsxs(_Fragment, { children: [assistantShown && (_jsx(AssistantStage, { character: assistantCharacter, state: assistantState, size: ASSISTANT_SIZE, place: stageDrag.position
|
|
732
|
+
? { left: stageDrag.position.left, top: stageDrag.position.top }
|
|
733
|
+
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
|
|
555
734
|
...getPositionStyles(),
|
|
556
735
|
}, children: _jsxs(Box, { sx: {
|
|
557
736
|
position: 'relative',
|
|
558
737
|
display: 'inline-flex',
|
|
559
738
|
// The glow's halos go behind the button, not behind the page.
|
|
560
739
|
isolation: 'isolate',
|
|
561
|
-
}, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [buttonGlow && _jsx(ButtonGlow, { color: brandColor }), _jsx(Tooltip, { text: `${buttonTooltip}${shortcutHint ? ` (${shortcutHint})` : ''}`, direction: position.includes('right') ? 'w' : 'e', children: _jsx(
|
|
562
|
-
? buttonIcon
|
|
563
|
-
: CommentDiscussionIcon, "aria-label": buttonTooltip, onClick: handleToggle, size: "large", sx: {
|
|
564
|
-
width: 56,
|
|
565
|
-
height: 56,
|
|
566
|
-
borderRadius: '50%',
|
|
567
|
-
bg: brandColor || 'accent.emphasis',
|
|
568
|
-
color: 'fg.onEmphasis',
|
|
569
|
-
boxShadow: 'shadow.large',
|
|
570
|
-
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
571
|
-
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
|
|
572
|
-
'&:hover': {
|
|
573
|
-
bg: brandColor || 'accent.emphasis',
|
|
574
|
-
boxShadow: 'shadow.extra-large',
|
|
575
|
-
},
|
|
576
|
-
} }) }), messages.length > 0 && (_jsx(Box, { sx: {
|
|
740
|
+
}, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [buttonGlow && _jsx(ButtonGlow, { color: brandColor }), isHovered && saying ? (_jsx(SpeechBalloon, { text: saying.text, more: saying.more, onOpen: handleToggle, above: 64, side: position.startsWith('bottom') ? 'above' : 'below', align: position.includes('right') ? 'right' : 'left', tailAt: 28 })) : null, saying ? (chatButton) : (_jsx(Tooltip, { text: `${buttonTooltip}${shortcutHint ? ` (${shortcutHint})` : ''}`, direction: position.includes('right') ? 'w' : 'e', children: chatButton })), messages.length > 0 && (_jsx(Box, { sx: {
|
|
577
741
|
position: 'absolute',
|
|
578
742
|
top: 0,
|
|
579
743
|
right: 0,
|
|
@@ -589,7 +753,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
589
753
|
fontSize: 0,
|
|
590
754
|
fontWeight: 'bold',
|
|
591
755
|
pointerEvents: 'none',
|
|
592
|
-
}, children: _jsx(Text, { sx: { fontSize: 0 }, children: messages.length > 99 ? '99+' : messages.length }) })),
|
|
756
|
+
}, children: _jsx(Text, { sx: { fontSize: 0 }, children: messages.length > 99 ? '99+' : messages.length }) })), unheard && (_jsx(Box, { sx: {
|
|
593
757
|
position: 'absolute',
|
|
594
758
|
top: 0,
|
|
595
759
|
left: 0,
|
|
@@ -610,6 +774,13 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
610
774
|
opacity: 0,
|
|
611
775
|
},
|
|
612
776
|
},
|
|
777
|
+
// Still, as a ring held around the button, for a reader
|
|
778
|
+
// who asks the system for reduced motion (T-28).
|
|
779
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
780
|
+
animation: 'none',
|
|
781
|
+
transform: 'scale(1.15)',
|
|
782
|
+
opacity: 0.6,
|
|
783
|
+
},
|
|
613
784
|
} }))] }) })), isMobile && isOpen && (_jsx(Box, { sx: {
|
|
614
785
|
position: 'fixed',
|
|
615
786
|
top: 0,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating assistant on the page (LOOP T-21, T-22, T-27): a character in
|
|
3
|
+
* a corner, acting out what the application is doing, with a balloon that
|
|
4
|
+
* says a word while the conversation is closed. Clicked, it opens the
|
|
5
|
+
* conversation; dragged, it moves, and the conversation follows; sent away,
|
|
6
|
+
* it says goodbye and leaves a small way back.
|
|
7
|
+
*
|
|
8
|
+
* Its motions are one set for every character, by the parts each drawing
|
|
9
|
+
* names (`assistant-body`, `-pupils`, `-lids`, `-mouth`), and nothing moves
|
|
10
|
+
* for a reader who asks the system for reduced motion.
|
|
11
|
+
*
|
|
12
|
+
* @module chat/assistant/AssistantStage
|
|
13
|
+
*/
|
|
14
|
+
import type { JSX, RefObject } from 'react';
|
|
15
|
+
import { type AssistantCharacter } from './characters';
|
|
16
|
+
import type { AssistantCharacterData } from './formats/types';
|
|
17
|
+
import type { AssistantAway, AssistantState } from './state';
|
|
18
|
+
/**
|
|
19
|
+
* Where the balloon goes so that it stays inside the window (T-23): toward
|
|
20
|
+
* the middle of the page from whichever half the character stands in, and
|
|
21
|
+
* below it when it stands too near the top.
|
|
22
|
+
*/
|
|
23
|
+
export declare function balloonSide(place: {
|
|
24
|
+
left?: unknown;
|
|
25
|
+
top?: unknown;
|
|
26
|
+
right?: unknown;
|
|
27
|
+
bottom?: unknown;
|
|
28
|
+
}, viewport?: {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}): {
|
|
32
|
+
side: 'above' | 'below';
|
|
33
|
+
align: 'left' | 'right';
|
|
34
|
+
};
|
|
35
|
+
export interface AssistantStageProps {
|
|
36
|
+
/** The character: one Datalayer ships, by id (T-25), or one loaded from a file (T-26). */
|
|
37
|
+
character: string | AssistantCharacter | AssistantCharacterData;
|
|
38
|
+
/** What it acts out (T-22). */
|
|
39
|
+
state: AssistantState;
|
|
40
|
+
/** Its size, in pixels. */
|
|
41
|
+
size?: number;
|
|
42
|
+
/** Where it sits once moved, or the corner it starts in. */
|
|
43
|
+
place: {
|
|
44
|
+
left: number;
|
|
45
|
+
top: number;
|
|
46
|
+
} | React.CSSProperties;
|
|
47
|
+
/** The element the drag measures. */
|
|
48
|
+
stageRef: RefObject<HTMLDivElement | null>;
|
|
49
|
+
/** Starts a drag. */
|
|
50
|
+
onDragStart: (event: React.PointerEvent<HTMLElement>) => void;
|
|
51
|
+
/** The conversation is open: the balloon above is the chat itself. */
|
|
52
|
+
open: boolean;
|
|
53
|
+
/** Open or close the conversation. */
|
|
54
|
+
onToggle: () => void;
|
|
55
|
+
/**
|
|
56
|
+
* What the balloon says while the conversation is closed: the agent's
|
|
57
|
+
* newest words, as the Office Assistant said them (T-23), or a welcome.
|
|
58
|
+
* `more` adds *Open the conversation* for the rest.
|
|
59
|
+
*/
|
|
60
|
+
balloon?: {
|
|
61
|
+
text: string;
|
|
62
|
+
more?: boolean;
|
|
63
|
+
};
|
|
64
|
+
/** Show the balloon without being hovered: something new to say. */
|
|
65
|
+
insist?: boolean;
|
|
66
|
+
/** Send it away (T-27): for the page, for the session, or for good. */
|
|
67
|
+
onDismiss: (away: AssistantAway) => void;
|
|
68
|
+
}
|
|
69
|
+
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, }: AssistantStageProps): JSX.Element;
|
|
70
|
+
export default AssistantStage;
|